ارسال ایمیل در ASP.NET Core
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

فضای نام System.Net.Mail در NET Core 1.2. که پیاده سازی netstandard2.0 است، ارائه خواهد شد. بنابراین فعلا (در زمان NET Core 1.1.) راه حل توکار و رسمی برای ارسال ایمیل در برنامه‌های مبتنی بر NET Core. وجود ندارد. اما می‌توان کتابخانه‌ی ثالثی را به نام MailKit، به عنوان راه‌حلی که .NET 4.0, .NET 4.5, .NET Core, Xamarin.Android, و Xamarin.iOS را پشتیبانی می‌کند، درنظر گرفت و توانمندی‌ها و پروتکل‌های پشتیبانی شده‌ی توسط آن، از System.Net.Mail توکار نیز بسیار بیشتر است.


افزودن وابستگی‌های MailKit به برنامه

برای شروع به استفاده‌ی از MailKit، می‌توان بسته‌ی نیوگت آن‌را به فایل project.json برنامه معرفی کرد:
{
    "dependencies": {
        "MailKit": "1.10.0"
    }
}


استفاده از MailKit جهت تکمیل وابستگی‌های ASP.NET Core Identity

قسمتی از ASP.NET Core Identity، شامل ارسال ایمیل‌های «ایمیل خود را تائید کنید» است که آن‌را می‌توان توسط MailKit به نحو ذیل تکمیل کرد:
using System.Threading.Tasks;
using ASPNETCoreIdentitySample.Services.Contracts.Identity;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
 
namespace ASPNETCoreIdentitySample.Services.Identity
{
    public class AuthMessageSender : IEmailSender, ISmsSender
    {
        public async Task SendEmailAsync(string email, string subject, string message)
        {
            var emailMessage = new MimeMessage();
 
            emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info"));
            emailMessage.To.Add(new MailboxAddress("", email));
            emailMessage.Subject = subject;
            emailMessage.Body = new TextPart(TextFormat.Html)
            {
                Text = message
            };
 
            using (var client = new SmtpClient())
            {
                client.LocalDomain = "dotnettips.info";
                await client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
                await client.SendAsync(emailMessage).ConfigureAwait(false);
                await client.DisconnectAsync(true).ConfigureAwait(false);
            }
        }
 
        public Task SendSmsAsync(string number, string message)
        {
            // Plug in your SMS service here to send a text message.
            return Task.FromResult(0);
        }
    }
}
در اینجا MimeMessage بیانگر محتوا و تنظیمات ایمیلی است که قرار است ارسال شود. ابتدا قسمت‌های From و To آن تنظیم می‌شوند تا مشخص باشد که ایمیل ارسالی از کجا ارسال شده و قرار است به چه آدرسی ارسال شود. در ادامه موضوع و عنوان ایمیل تنظیم شده‌است. سپس متن ایمیل، به خاصیت Body شیء MimeMessage انتساب داده خواهد شد. فرمت ایمیل ارسالی را نیز می‌توان در اینجا تنظیم کرد. برای مثال TextFormat.Html جهت ارسال پیام‌هایی حاوی تگ‌های HTML مناسب است و اگر قرار است صرفا متن ارسال شود، می‌توان TextFormat.Plain را انتخاب کرد.
در آخر، این پیام به SmtpClient جهت ارسال نهایی، فرستاده می‌شود. این SmtpClient هرچند هم نام مشابه آن در System.Net.Mail است اما با آن یکی نیست و متعلق است به MailKit. در اینجا ابتدا LocalDomain تنظیم شده‌است. تنظیم این مورد اختیاری بوده و صرفا به SMTP سرور دریافت کننده‌ی ایمیل‌ها، مرتبط است که آیا قید آن‌را اجباری کرده‌است یا خیر. تنظیمات اصلی SMTP Server در متد ConnectAsync ذکر می‌شوند که شامل مقادیر host ،port و پروتکل ارسالی هستند.


ارسال ایمیل به SMTP pickup folder

روشی که تا به اینجا بررسی شد، جهت ارسال ایمیل‌ها به یک SMTP Server واقعی کاربرد دارد. اما در حین توسعه‌ی محلی برنامه می‌توان ایمیل‌ها را در داخل یک پوشه‌ی موقتی ذخیره و آن‌ها را توسط برنامه‌ی Outlook (و یا حتی مرورگر Firefox) بررسی و بازبینی کامل کرد.
در این حالت تنها کاری را که باید انجام داد، جایگزین کردن قسمت ارسال ایمیل واقعی توسط SmtpClient در کدهای فوق، با قطعه کد ذیل است:
using (var stream = new FileStream($@"c:\smtppickup\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew))
{
   emailMessage.WriteTo(stream);
}
تولید فایل‌های eml جهت «شبیه سازی ارسال ایمیل در ASP.Net» بسیار مفید هستند.


FAQ و منبع تکمیلی
  • #
    ‫۷ سال و ۵ ماه قبل، جمعه ۲۵ فروردین ۱۳۹۶، ساعت ۲۳:۵۰
    جهت اطلاع
    مایکروسافت به صورت رسمی SmtpClient را منسوخ شده اعلام کرده و توصیه می‌کند که از MailKit استفاده کنید:
    [System.Obsolete("SmtpClient and its network of types are poorly designed, we strongly recommend you use https://github.com/jstedfast/MailKit and https://github.com/jstedfast/MimeKit instead")]
    public class SmtpClient : IDisposable
    ماخذ (مستندات دات نت 4.7)
    • #
      ‫۲ سال قبل، دوشنبه ۲۱ شهریور ۱۴۰۱، ساعت ۱۵:۵۵
      از کدهای زیر در پروژه core 5  استفاده شده است و افزونه دیگری نصب نشده است و فقط از smtpClient استفاده شده است و مشکلی در اجرا نداشته اند و ایمیل به خوبی ارسال شد:
      var smtp = new SmtpClient {
        Host = StaticValues.HostEmail,
          Port = StaticValues.PortEmail,
          EnableSsl = false, //StaticValues.SslEmail,
          DeliveryMethod = SmtpDeliveryMethod.Network,
          UseDefaultCredentials = false,
          Credentials = new NetworkCredential(fromEmail, EmailPassword)
      };
      using(var message = new MailMessage(fromEmail, toEmail, subject, body) {
        BodyEncoding = Encoding.UTF8,
          IsBodyHtml = true,
          HeadersEncoding = Encoding.UTF8,
          DeliveryNotificationOptions = DeliveryNotificationOptions.OnFailure
      }) {
        smtp.Send(message);
      }
  • #
    ‫۶ سال و ۵ ماه قبل، چهارشنبه ۲۹ فروردین ۱۳۹۷، ساعت ۱۸:۴۷
    خلاصه نکات این مطلب در برنامه‌های ASP.NET Core

    ابتدا بسته‌ی نیوگت DNTCommon.Web.Core را نصب کنید:
    PM> Install-Package DNTCommon.Web.Core
    سپس مثالی از IWebMailService آن‌را در اینجا می‌توانید مشاهده کنید.  
  • #
    ‫۳ سال و ۱۱ ماه قبل، دوشنبه ۳۱ شهریور ۱۳۹۹، ساعت ۰۱:۳۰
    نکته تکمیلی:
    در صورت استفاده از mailkit در محیط net core. میتوان کلاس SMTPClient را به شکل زیر مورد استفاده قرار داد.
     public class DiskSmtpClient : SmtpClient
        {
            public DiskSmtpClient(IOptionsSnapshot<MailKitOptions> mailOptionsSnapshot)
            {
                if (mailOptionsSnapshot.Value.SpecifiedPickupDirectory)
                {
                    SpecifiedPickupDirectory = true;
                    PickupDirectoryLocation = mailOptionsSnapshot.Value.PickupDirectoryLocation;
                }
                
            }
            public bool SpecifiedPickupDirectory { get; set; }
            public string PickupDirectoryLocation { get; set; }
    
            public override Task SendAsync(MimeMessage message, CancellationToken cancellationToken = new CancellationToken(),
                ITransferProgress progress = null)
            {
                if (!SpecifiedPickupDirectory)
                    return base.SendAsync(message, cancellationToken, progress);
                return SaveToPickupDirectory(message, PickupDirectoryLocation);
    
            }
    
        
    
    
    
            private async Task SaveToPickupDirectory(MimeMessage message, string pickupDirectory)
            {
                using (var stream = new FileStream($@"{pickupDirectory}\email-{Guid.NewGuid().ToString("N")}.eml", FileMode.CreateNew))
                {
                    await message.WriteToAsync(stream);
                }
            }
    
           
    
            public override Task ConnectAsync(string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto,
                CancellationToken cancellationToken = new CancellationToken())
            {
                if (!SpecifiedPickupDirectory)
                    return base.ConnectAsync(host, port, options, cancellationToken);
                return Task.CompletedTask;
            }
    
    
    
            public override Task DisconnectAsync(bool quit, CancellationToken cancellationToken = new CancellationToken())
            {
                if (!SpecifiedPickupDirectory)
                    return base.DisconnectAsync(quit, cancellationToken);
    
                return Task.CompletedTask;
            }
        }
    در کد بالا دو خصوصیت SpecifiedPickupDirectory  و PickupDirectoryLocation به آن اضافه شده اند و با رونویسی از متدهای مورد استفاده به جای ارسال ایمیل در صورت مقداردهی  SpecifiedPickupDirectory  ایمیل در آدرس  PickupDirectoryLocation   ذخیره میگردد. سپس به شکل زیر آن را مورد استفاده قرار میدهیم:
     services.AddTransient<DiskSmtpClient>();
      var email = "mail@dotnettips.info";
                var subject = "subject";
                var message = "message";
    
                var emailMessage = new MimeMessage();
    
                emailMessage.From.Add(new MailboxAddress("DNT", "do-not-reply@dotnettips.info"));
                emailMessage.To.Add(new MailboxAddress("", email));
                emailMessage.Subject = subject;
                emailMessage.Body = new TextPart(TextFormat.Html)
                {
                    Text = message
                };
    
                
                    _client.SpecifiedPickupDirectory = true;
                    _client.PickupDirectoryLocation = "c:\\mail";
    
                    _client.LocalDomain = "dotnettips.info";
                    await _client.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
                    await _client.SendAsync(emailMessage).ConfigureAwait(false);
                    await _client.DisconnectAsync(true).ConfigureAwait(false);
    در صورتی که قصد ندارید کد اضافه‌تری را نیز اعمال نمایید میتوانید با اضافه کردن تکه کد زیر به فایل startup و محتوای تنظیمات آن به فایل appsettings.json دو خط بالا را حذف نمایید:
    services.Configure<MailKitOptions>(options => Configuration.GetSection("MailKitOptions").Bind(options));
      "MailKitOptions": {
        "SpecifiedPickupDirectory": true,
        "PickupDirectoryLocation": "c:\\mail"
      }
    در این صورت میتوان تنظیمات جداگانه ای برای حالت انتشار و توسعه نیز در نظر گرفت.
    کلاس متناظر MailKitOptions
        public class MailKitOptions
        {
            public  bool SpecifiedPickupDirectory { get; set; }
            public  string PickupDirectoryLocation { get; set; }
        }

    • #
      ‫۳ سال و ۸ ماه قبل، سه‌شنبه ۲ دی ۱۳۹۹، ساعت ۲۱:۴۶
      ظاهرا به خاطر استفاده از سرویس IHttpContextAccessor  در ویو رندر سرویس که پس این سرویس ارسال ایمیل به کار برده شده، نمیشه به صورت بدون await ازین توابع ارسال استفاده کرد، چون در حالت فعلی ارسال ایمیل تا اتمام کارش باعث قفل UI میشه و حالتی رو فرض کنید که با توجه به نتیجه‌ی اکشن قصد ارسال چندین ایمیل به چندین کاربر رو داریم (برای مثال نوتیفیکیشن تغییر وضعیت کالا)، پس نمیخایم از نتایج ارسال ایمیل‌ها آگاه بشیم و فقط قصد ارسال داریم، در صورت اگه با await استفاده بشن، UI قفل میشه و اگه بدون Await استفاده بشن، چون کار اکشن تمام شده و مثلا Return view انجام شده و هنوز ارسال ایمیل‌ها ناتمام هست، با خطای که نشان دهنده‌ی null بودن سرویس پرووایدر هست روبرو میشه.
      لطفا راهنمایی کنید. ممنون
        • #
          ‫۳ سال و ۸ ماه قبل، چهارشنبه ۳ دی ۱۳۹۹، ساعت ۰۱:۰۴
          ممنون از راهنمایی شما و با تشکر از تهیه سرویس‌های مفید در کتابخانه DNT

          مطلب اشاره شده رو مطالعه کردم اما چند اختلاف داره با چیزی که بنده لازم دارم:
          برای مثال حالتی رو در نظر بگیرید که مثلا پس از موجود شدن محصول و تغییر وضعیت اون، باید به تمام کاربرانی که درخواست اطلاع رسانی کردن برای اون محصول، ایمیل ارسال بشه و طبعا نه تعداد ایمیل‌ها مشخص هست و نه زمان دقیق این کار:

          اول اینکه فرمودید ارسال ایمیل آنی است: برای یک ایمیل طبعا چنین به نظر میاد ولی برای تعداد بالای ایمیل، UI قفل میشه با استفاده از سرویس WebMailService  در کتابخانه‌ی DNT و البته برای اجتناب از این مشکل نمیشه هم که بدون await ازش استفاده کرد به خاطر نیازمندی‌های سرویس رندرویو در این کتابخانه.

          دوم اینکه عنوان شده بود که سرویس هایی که به صورت بکگراند معرفی میشن غالبا غیر HTTP هستن و اما بنده به سرویس HttpContextAccessor نیاز دارم برای کار مورد نظرم و از طرفی خود شما هم در ارسال ایمیل به صورت قالب‌های از پیش تعیین شده HTML و در داخل سرویس ویورند کتابخانه DNT از HttpContextAccessor   استفاده کردید. 

          سوم اینکه در این مطلب آمده بود که سرویس هایی که به صورت بکگراند معرفی میشن با اجرای برنامه، اجرا و درحافظه قرار میگیرن تا زمان خاتمه برنامه، حتی اگر کاری برای انجام نداشته باشن. در مورد مثال بالا هیچ مشخص نیست که کی به ارسال یک یا تعداد زیادی ایمیل احتیاج داریم، چرا که ممکنه محصولات مدت‌ها تغییری در وضعیتشون ایجاد نشه و طبعا در این صورت قرار دادن سرویس به صورت بکگراند در حافظه منطقی به نظر نمیاد.

          چهارم اینکه در این نوع سرویس‌ها ظاهرا فقط یکبار در ابتدای برنامه متد ExecuteAsync   اجرا میشه که تعداد و نوع پارامترهای ورودیش مشخصه که فقط یک CancellationToken هست و بنابراین چطور میشه بهش اطلاع داد که چه زمانی (نه به صورت زمان بندی شده و بلکه به صورت آنی) کار مورد نظر رو انجام بده  (مثلا ارسال ایمیل از طریق سرویس  WebMailService موجود در کتابخانه DNT شما به تعدادی کاربر)

          و آخر اینکه در زمان نیاز به انجام کاری، معمولا تعدادی هم پارامتر وجود داره برای انجام کار. (مثلا شناسه محصولی که تغییر وضعیت داشته جهت استخراج مشخصات کاربرانی که اطلاع رسانی این محصول رو فعال کردن). چطور این پارامتر هارو باید به سرویس بکگراند انتقال داد!!

          ممنون از حوصله و راهنمایی شما.
          • #
            ‫۳ سال و ۸ ماه قبل، چهارشنبه ۳ دی ۱۳۹۹، ساعت ۰۲:۰۲
            - اگر این کتابخانه برای شما مفید نیست؛ از آن استفاده نکنید. ما راه حل دیگری نداریم. جای دیگری هم چیزی دیگری پیدا نمی‌کنید که در اساس با این راه حل متفاوت باشد.
            - هدف از await را بهتر است در دوره‌ای که معرفی کردم (مبانی Async در C# 5) مطالعه کنید؛ چون به نظر آشنایی با آن ندارید. وجود آن نه مشکلی است و نه ربطی به قفل کردن UI دارد. البته اگر به نحو نادرستی مورد استفاده قرار گیرد، می‌تواند باعث قفل شدن UI هم شود که در آن سری با مثال بررسی شده.
            - جائیکه HttpContext وجود ندارد، از آن استفاده نکنید. قالب رشته‌ای نهایی را به نحو دیگری تولید کنید.
            - در این سایت برای تک ایمیل‌ها، از روش استفاده از سرویس‌های ارسال ایمیلی مانند مطلب جاری استفاده می‌شود (مانند ایمیلی که هم اکنون جهت اطلاع رسانی دریافت پاسخی به شما ارسال شد). برای ایمیل‌های با تعداد بالا از کتابخانه‌ی «DNT Scheduler» استفاده می‌شود که نسخه‌ی Core هم دارد. در Taskهایی که در اینجا تعریف می‌شوند، از بانک اطلاعاتی کوئری بگیرید و پارامترها را از آن‌ها استخراج کنید (در زمان‌هایی مشخص کوئری می‌گیرید که آیا زمان ارسال ایمیل هست یا خیر؟ اگر بله اطلاعات بیشتر را از بانک اطلاعاتی دریافت و استفاده کنید). از این لحاظ محدودیتی ندارد. مطلب «انجام کارهای پس‌زمینه» هم که عنوان شد نمونه‌ی دیگری از این نوع پیاده سازی‌ها است؛ جهت آشنایی بیشتر.
          • #
            ‫۳ سال و ۸ ماه قبل، چهارشنبه ۳ دی ۱۳۹۹، ساعت ۲۱:۲۳
            من برای کارهای شبیه آنچه شما می‌خواهید از یک ویندوز سرویس استفاده میکنم. در کنار هر سایت یک ویندوز سرویس دارم برای کارهای اینچنینی. سایت به ویندوز سرویس فرمان می‌دهد که کاری را انجام دهد.
            خوبی ویندوز سرویس این است که همیشه در حال اجراست و مستقل از سایت کارش را انجام می‌دهد و اگر لازم باشد کارهای ارسالی از سایت را هم میتواند در صف نگهدارد برای انجام شدن.
          • #
            ‫۳ سال و ۸ ماه قبل، پنجشنبه ۴ دی ۱۳۹۹، ساعت ۱۴:۵۴
            از IBackgroundQueueService هم می‌توانید برای این نوع کارهای موقتی طولانی مدت در پس‌زمینه استفاده کنید.
  • #
    ‫۳ سال و ۶ ماه قبل، سه‌شنبه ۵ اسفند ۱۳۹۹، ساعت ۲۲:۳۵
    سلام؛ من از پروژه DNTCommon . Web . Core در وبسایتم استفاده کردم. ارسال ایمیل به صورت لوکال و در فایل به درستی کار میکنه؛ ولی در سرور و ارسال ایمیل واقعی، خطای زیر رخ می‌دهد. در ضمن برای ssl از lets encrypt در سرور استفاده شده:
    An unhandled exception has occurred while executing the request. 
    MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection. 
    The host name did not match the name given in the server's SSL certificate.
    • #
      ‫۳ سال و ۶ ماه قبل، سه‌شنبه ۵ اسفند ۱۳۹۹، ساعت ۲۳:۱۳
      - خطای «The host name did not match the name given in the server's SSL certificate»، به معنای دستکاری شدن مجوز دریافتی از میل سرور راه دور، در بین راه هست و یا یکی از این 5 مورد که به معنای به روز نبودن سیستم عامل سرور هست یا مشکلات شبکه داخلی.
      - راه حل‌هایی مانند کدهای زیر هم برای عدم بررسی مجوزهای دریافتی وجود دارد (که به معنای لغو بررسی‌های امنیتی است):
      using (var client = new SmtpClient ()) {
          client.ServerCertificateValidationCallback = (s,c,h,e) => true;
          client.CheckCertificateRevocation = false;
          // ...
      }
      • #
        ‫۳ سال و ۶ ماه قبل، چهارشنبه ۶ اسفند ۱۳۹۹، ساعت ۱۸:۰۸
        ممنونم;خیلی بهتر میشد اگه توی appsetting قابل تنظیم باشه. چون پروژه common به صورت پکیج اضافه میشن.
        یه مورد دیگه، این امنیت مورد خاصی نداره اگه غیر فعال باشه؟ مشکلی پیش نمیاد
        • #
          ‫۳ سال و ۶ ماه قبل، پنجشنبه ۷ اسفند ۱۳۹۹، ساعت ۰۰:۱۵
          - مدتی هست که کار با پروتکل SSL و TLS، از داخل، با مشکل مواجه شده:

          این نوع خطاها را اگر مشاهده کردید، مشکل از برنامه یا کتابخانه‌ی خاصی نیست. مشکلات شبکه‌ی داخلی هست.

          + پارامتر shouldValidateServerCertificate اضافه شد.