Приложение С# для отправки электронной почты по расписанию

У меня есть консольное приложение С#, работающее на Windows Server 2003, целью которого является чтение таблицы с названием "Уведомления" и поля "NotifyDateTime" и отправка электронной почты, когда это время будет достигнуто. Я планировал его с помощью планировщика заданий выполнять ежечасно, проверять, не замечен ли NotifyDateTime в этот час, а затем отправлять уведомления.

Похоже, потому что у меня есть дата/время уведомления в базе данных, что должен быть лучший способ, чем повторять эту вещь каждый час.

Есть ли легкое приложение процесса/консоли, которое я могу оставить на сервере, который читает в дневных уведомлениях из таблицы и выдает их точно, когда они должны?

Я думал, что это сервис, но это кажется излишним.

Ответ 1

Мое предложение - написать простое приложение, которое использует Quartz.NET.

Создайте 2 задания:

  • Во-первых, срабатывает один раз в день, читает все ожидающие время уведомления из базы данных, запланированной на этот день, создает на ней триггеры.
  • Во-вторых, зарегистрированный для таких триггеров (подготовленный первым заданием), отправляет ваши уведомления.

Что еще,

Я настоятельно рекомендую вам создать службу Windows для этой цели, просто чтобы не было постоянно запущено одиночное консольное приложение. Он может быть случайно завершен кем-то, у кого есть доступ к серверу под той же учетной записью. Что еще, если сервер будет перезагружен, вы должны помнить о том, чтобы включить такое приложение снова, вручную, в то время как служба может быть настроена на автоматический запуск.

Если вы используете веб-приложение, вы всегда можете использовать эту логику, например. в процессе пула приложений IIS, хотя это плохая идея. Это потому, что по умолчанию этот процесс периодически перезапускается, поэтому вы должны изменить его конфигурацию по умолчанию, чтобы убедиться, что он все еще работает посреди ночи, когда приложение не используется. Если ваши запланированные задачи не будут завершены.

ОБНОВЛЕНИЕ (примеры кода):

Класс диспетчера, внутренняя логика для планирования и незапланированных заданий. По соображениям безопасности, реализованным в качестве одноэлементного:

internal class ScheduleManager
{
    private static readonly ScheduleManager _instance = new ScheduleManager();
    private readonly IScheduler _scheduler;

    private ScheduleManager()
    {
        var properties = new NameValueCollection();
        properties["quartz.scheduler.instanceName"] = "notifier";
        properties["quartz.threadPool.type"] = "Quartz.Simpl.SimpleThreadPool, Quartz";
        properties["quartz.threadPool.threadCount"] = "5";
        properties["quartz.threadPool.threadPriority"] = "Normal";

        var sf = new StdSchedulerFactory(properties);
        _scheduler = sf.GetScheduler();
        _scheduler.Start();
    }

    public static ScheduleManager Instance
    {
        get { return _instance; }
    }

    public void Schedule(IJobDetail job, ITrigger trigger)
    {
        _scheduler.ScheduleJob(job, trigger);
    }

    public void Unschedule(TriggerKey key)
    {
        _scheduler.UnscheduleJob(key);
    }
}

Первое задание, для сбора необходимой информации из базы данных и планирования уведомлений (второе задание):

internal class Setup : IJob
{
    public void Execute(IJobExecutionContext context)
    {
        try
        {                
            foreach (var kvp in DbMock.ScheduleMap)
            {
                var email = kvp.Value;
                var notify = new JobDetailImpl(email, "emailgroup", typeof(Notify))
                    {
                        JobDataMap = new JobDataMap {{"email", email}}
                    };
                var time = new DateTimeOffset(DateTime.Parse(kvp.Key).ToUniversalTime());
                var trigger = new SimpleTriggerImpl(email, "emailtriggergroup", time);
                ScheduleManager.Instance.Schedule(notify, trigger);
            }
            Console.WriteLine("{0}: all jobs scheduled for today", DateTime.Now);
        }
        catch (Exception e) { /* log error */ }           
    }
}

Второе задание, для отправки писем:

internal class Notify: IJob
{
    public void Execute(IJobExecutionContext context)
    {
        try
        {
            var email = context.MergedJobDataMap.GetString("email");
            SendEmail(email);
            ScheduleManager.Instance.Unschedule(new TriggerKey(email));
        }
        catch (Exception e) { /* log error */ }
    }

    private void SendEmail(string email)
    {
        Console.WriteLine("{0}: sending email to {1}...", DateTime.Now, email);
    }
}

Макет базы данных, только для целей данного конкретного примера:

internal class DbMock
{
    public static IDictionary<string, string> ScheduleMap = 
        new Dictionary<string, string>
        {
            {"00:01", "[email protected]"},
            {"00:02", "[email protected]"}
        };
}

Основная запись приложения:

public class Program
{
    public static void Main()
    {
        FireStarter.Execute();
    }
}

public class FireStarter
{
    public static void Execute()
    {
        var setup = new JobDetailImpl("setup", "setupgroup", typeof(Setup));
        var midnight = new CronTriggerImpl("setuptrigger", "setuptriggergroup", 
                                           "setup", "setupgroup",
                                           DateTime.UtcNow, null, "0 0 0 * * ?");
        ScheduleManager.Instance.Schedule(setup, midnight);
    }
}

Вывод:

enter image description here

Если вы собираетесь использовать службу, просто поместите эту основную логику в метод OnStart (советую запустить фактическую логику в отдельном потоке, не дожидаясь начала службы, и то же самое избежать возможных тайм-аутов - не в этом конкретном примере, очевидно, но в целом):

protected override void OnStart(string[] args)
{
    try
    {
        var thread = new Thread(x => WatchThread(new ThreadStart(FireStarter.Execute)));
        thread.Start();
    }
    catch (Exception e) { /* log error */ }            
}

Если это так, инкапсулируйте логику в некоторую оболочку, например. WatchThread, который поймает любые ошибки из потока:

private void WatchThread(object pointer)
{
    try
    {
        ((Delegate) pointer).DynamicInvoke();
    }
    catch (Exception e) { /* log error and stop service */ }
}

Ответ 2

Вы пытаетесь реализовать подход опроса, когда задание контролирует запись в БД для любых изменений.

В этом случае мы пытаемся ударить DB на периодическое время, поэтому, если одночасовая задержка уменьшится до 1 минуты на более поздней стадии, тогда это решение превратится в горлышко с производительностью.

Подход 1

Для этого сценария используйте подход на основе очереди, чтобы избежать каких-либо проблем, вы также можете увеличить количество экземпляров, если вы отправляете столько писем.

Я понимаю, что в таблице есть обновления программы NotifyDateTime, одна и та же программа может отправить сообщение в очередь, сообщив, что есть уведомление для обработки.

Существует служба Windows, которая следит за этой очередью для любых входящих сообщений, когда есть сообщение, которое выполняет требуемую операцию (т.е. отправляет электронную почту).

Подход 2

http://msdn.microsoft.com/en-us/library/vstudio/zxsa8hkf(v=vs.100).aspx

вы также можете вызывать код С# из хранимой процедуры SQL Server, если вы используете MS SQL Server. но в этом случае вы используете свой процесс SQL-сервера для отправки почты, что не является хорошей практикой.

Однако вы можете вызвать веб-службу или службу WCF, которая может отправлять электронные письма.

Но Подход 1 не содержит ошибок, масштабируется, отслеживается, асинхронен и не беспокоит вашу базу данных ИЛИ APP, у вас есть другой процесс отправки электронной почты.

Очереди

Использовать MSMQ, который является частью сервера Windows

Вы также можете попробовать https://www.rabbitmq.com/dotnet.html

Ответ 3

Предварительно запланированные задачи (в undefined раза) обычно являются болью для обработки, в отличие от запланированных задач, где Quartz.NET кажется хорошо подходящим.

Кроме того, необходимо провести другое различие между задачами, которые не должны прерываться/меняться (например, повторы, уведомления) и задачи, которые необходимо активно контролировать (например, кампания или связь).

Для задач типа "огонь-и-забыть" очередь сообщений хорошо подходит. Если назначение ненадежно, вам нужно будет выбрать уровни повтора (например, отправьте попытку (максимум два раза), повторите попытку через 5 минут, попробуйте отправить (максимум два раза), повторите попытку через 15 минут), что, по крайней мере, требует указания специфического TTL сообщения с помощью очереди отправки и повтора. Здесь объяснение с ссылкой на код для установки очереди уровня повтора

Управляемые запланированные задачи потребуют использования подхода к базе данных (Нажмите здесь для статьи CodeProject по созданию очереди баз данных для запланированных задач) , Это позволит вам обновлять, удалять или переназначать уведомления, если вы будете отслеживать идентификаторы владельца (например, указать идентификатор пользователя, и вы можете удалить все ожидающие уведомления, когда пользователь больше не будет получать уведомления, такие как скончание/отмена подписки)

Запланированные задачи электронной почты (включая любые коммуникационные задачи) требуют более тонкого управления (механизмы истечения, повтора и тайм-аута). Наилучший подход для этого - создать конечный автомат, способный обрабатывать задачу электронной почты через свои действия (истечение, предварительная валидация, предварительные рассылки, такие как templating, inlining css, абсолютная связь ссылок, добавление объектов отслеживания для открытого отслеживания, сокращения ссылок для отслеживания кликов, последующей проверки и отправки и повторной попытки).

Надеюсь, вы знаете, что .NET SmtpClient не полностью соответствует спецификациям MIME и что вы должны использовать поставщик электронной почты SAAS, такой как Amazon SES, Mandrill, Mailgun, Customer.io или Sendgrid. Я бы посоветовал вам взглянуть на Mandrill или Mailgun. Также, если у вас есть время, посмотрите MimeKit, который вы можете использовать для создания сообщений MIME для поставщиков, и не обязательно поддерживает такие вещи, как вложения/пользовательские заголовки/подпись DKIM.

Надеюсь, это приведет вас к правильному пути.

Edit

Вам нужно будет использовать услугу для опроса через определенные промежутки времени (например, 15 секунд или 1 минуту). Загрузка базы данных может быть несколько сведена путем проверки определенного количества заданных задач одновременно и сохранения внутреннего пула сообщений, подлежащих отправке (с механизмом тайм-аута на месте). Когда нет сообщений, возвращаемых, просто "спящий" опрос какое-то время. Я бы посоветовал не создавать такую ​​систему против одной таблицы в базе данных - вместо этого создайте независимую систему планирования электронной почты, с которой вы можете интегрироваться.

Ответ 4

Вместо этого я превратил бы это в сервис. Вы можете использовать обработчик событий System.Threading.Timer для каждого запланированного времени.

Ответ 5

Запланированные задачи можно планировать запускать только один раз в определенное время (в отличие от почасового, ежедневного и т.д.), поэтому одним из вариантов было бы создание запланированной задачи при изменении конкретного поля в базе данных.

Вы не указываете, какую базу данных вы используете, но некоторые базы данных поддерживают понятие триггера, например. в SQL: http://technet.microsoft.com/en-us/library/ms189799.aspx

Ответ 6

Если вы знаете, когда нужно отправлять электронные письма заблаговременно, я предлагаю вам использовать wait на дескрипторе события с соответствующим таймаутом. В полночь посмотрите на таблицу, а затем подождите, когда обработчик события истечет с истечением времени ожидания, когда нужно отправить следующее письмо. После отправки электронной почты снова подождите, когда тайм-аут будет установлен на следующую почту, которую следует отправить.

Кроме того, на основе вашего описания это, вероятно, должно быть реализовано как служба, но это не требуется.

Ответ 7

Я имею дело с той же проблемой около трех лет назад. Я несколько раз менял процесс, прежде чем он был достаточно хорош, я говорю вам, почему:

  • В первой реализации использовался специальный деамон из веб-хостинга, который назывался веб-сайтом IIS. Веб-сайт проверил IP-адрес вызывающего абонента, а затем проверил базу данных и отправил электронные письма. Это работало до одного дня, когда я получил много грязных писем от пользователей, которые я полностью спамал в своих почтовых ящиках. Недостаток хранения электронной почты в базе данных и отправка по электронной почте SMTP заключается в том, что существует НИЧЕГО, которые обеспечивают транзакцию DB к SMTP. Вы никогда не уверены в том, что письмо было успешно отправлено или нет. Отправка электронной почты может быть успешной, может быть неудачной или может быть ложной или может быть ложной отрицательной (клиент SMTP сообщает вам, что письмо не было отправлено, но оно было). Была проблема с SMTP-сервером, и сервер вернул false (email не отправил), но было отправлено электронное письмо. Демон отправил электронную почту каждый час за весь день до появления грязных писем.

  • Вторая реализация. Чтобы предотвратить спам, я изменил алгоритм, что письмо считается отправленным, даже если оно не удалось (мое уведомление по электронной почте было не слишком важным). Мой первый совет: "Не запускайте деамон слишком часто, потому что эта ложно-отрицательная ошибка smtp заставляет пользователей расстраиваться".

  • Через несколько месяцев на сервере произошли некоторые изменения, и демон не работал хорошо. Я получил идею из stackoverflow: привяжите .NET-таймер к домену веб-приложения. Это было не очень хорошо, потому что кажется, что IIS может время от времени перезапускать приложение из-за утечек памяти, и таймер никогда не срабатывает, если перезапускаются чаще, чем таймеры.

  • Последняя реализация. Планировщик Windows каждый час запускает пакет python, который читает локальный веб-сайт. Этот пожарный код ASP.NET.. Преимущество заключается в том, что планировщик окон времени вызывает надежную локальную партию и веб-сайт. IIS не зависает, он имеет возможность перезапуска. Сайт таймера является частью моего веб-сайта, это все еще один проект. (вместо этого вы можете использовать консольное приложение). Просто лучше. Он просто работает!

Ответ 8

Ваш первый выбор - правильный вариант, на мой взгляд. Планировщик заданий - рекомендуемый способ MS для выполнения периодических заданий. Кроме того, он гибкий, может сообщать о сбоях в операциях, оптимизируется и амортизируется среди всех задач в системе,...

Создание любого консольного приложения, которое работает все время, является хрупким. Он может быть выключен кем угодно, нуждается в открытом виде, не перезапускается автоматически,...

Другой вариант - создать какой-то сервис. Он гарантированно работает все время, так что это, по крайней мере, будет работать. Но какова была ваша мотивация?

"Похоже, потому что у меня есть дата/время уведомления в базе данных, что должен быть лучший способ, чем повторять эту задачу каждый час".

О, да, оптимизация... Итак, вы хотите добавить новый постоянно работающий сервис на свой компьютер, чтобы вы избегали одного потенциально невыполненного SQL-запроса каждый час? Лечение выглядит хуже, чем болезнь для меня.

И я не упомянул все недостатки службы. С одной стороны, ваша задача не использует ресурс, когда он не запускается. Это очень простой, легкий и эффективный запрос (при условии, что у вас есть нужный индекс).

С другой стороны, если ваша служба аварийно завершает работу, это, вероятно, ушло навсегда. Он нуждается в том, чтобы получать уведомления о новых электронных письмах, которые, возможно, необходимо отправить раньше запланированных. Он постоянно использует компьютерные ресурсы, такие как память. Хуже того, он может содержать утечки памяти.

Я думаю, что соотношение затрат и выгод очень низкое для любого решения, кроме тривиальной периодической задачи.