عنوان:

‫نصب و پیکربندی Postfix برای ارسال ایمیل - قسمت ۱۰ - ارسال ایمیل از یک برنامه #C در لینوکس با Postfix


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۴/۲۳ ۱۰:۱۰
آدرس: www.dntips.ir
در فصل‌های گذشته، ما یک سرور ایمیل Postfix را راه‌اندازی، امن و بهینه کردیم. در این فصل، یاد می‌گیریم که چگونه از این سرور قدرتمند در یک برنامه واقعی استفاده کنیم. ما یک برنامه ساده به زبان #C روی همان سرور لینوکسی می‌نویسیم و با استفاده از کتابخانه محبوب MailKit، ایمیل‌ها را از طریق Postfix محلی خودمان ارسال می‌کنیم. پیش از اینکار نیاز است با فایل main.cf و پارامترهای کلیدی آن بیشتر آشنا شویم تا بتوانیم بر اساس «تحلیل پیکربندی امنیتی Postfix» پارامترهای MailKit را تنظیم کنیم.

کالبدشکافی فایل main.cf و پارامترهای کلیدی

در تمام فصل‌های گذشته، ما به صورت مستقیم یا غیرمستقیم با فایل main.cf در ارتباط بوده‌ایم. این فایل، مغز متفکر و مرکز فرماندهی Postfix است. درک صحیح ساختار و پارامترهای مهم آن، به شما قدرت و انعطاف‌پذیری لازم برای پیکربندی هر نوع سناریوی ایمیلی را می‌دهد.

آشنایی با main.cf و ابزار postconf

فایل main.cf که در مسیر /etc/postfix/ قرار دارد، فایل پیکربندی اصلی Postfix است. ساختار این فایل بسیار ساده است:
parameter = value
هر خط نشان‌دهنده یک پارامتر و مقدار اختصاص داده شده به آن است. خطوطی که با # شروع می‌شوند، به عنوان کامنت در نظر گرفته شده و نادیده گرفته می‌شوند.
با اینکه صدها پارامتر برای Postfix وجود دارد، ویرایش مستقیم این فایل با nano یا vim می‌تواند منجر به خطاهای نوشتاری شود. ابزار استاندارد و امن برای کار با این فایل، دستور postconf است.

مشاهده تنظیمات با postconf:
مشاهده مقدار یک پارامتر خاص:
postconf myhostname

مشاهده تمام تنظیماتی که از مقدار پیش‌فرض خود تغییر کرده‌اند (بسیار کاربردی):
postconf -n

مشاهده تمام مقادیر پیش‌فرض Postfix:
postconf -d

ویرایش تنظیمات با postconf:
برای تغییر یا افزودن یک پارامتر، از آپشن -e (مخفف edit) استفاده می‌کنیم. این روش از بروز خطا جلوگیری می‌کند.
sudo postconf -e "myorigin = \$mydomain"
(نکته: علامت \ قبل از $ برای جلوگیری از تفسیر آن توسط Shell ضروری است.)
پس از هر تغییر در main.cf، باید از Postfix بخواهیم که تنظیمات جدید را بازخوانی کند:
sudo postfix reload

پارامترهای هویتی اصلی (Core Identity)

این پارامترها به Postfix می‌گویند که "کیست" و به کدام دامنه تعلق دارد. تنظیم اشتباه آن‌ها، ریشه بسیاری از مشکلات است.

myhostname
  • توضیح: این باید نام دامنه کاملاً معتبر (FQDN) سرور شما باشد. این همان نامی است که سرور شما هنگام معرفی خود در پروتکل SMTP (دستور HELO یا EHLO) استفاده می‌کند.
  • مقدار صحیح:mail.yourdomain.com

mydomain
  • توضیح: این پارامتر، نام دامنه اینترنتی شما بدون پیشوند نام میزبان است. در اکثر مواقع، Postfix به صورت هوشمند آن را از روی myhostname استخراج می‌کند.
  • مقدار پیش‌فرض (معمولاً): اگر myhostname برابر mail.yourdomain.com باشد، mydomain به صورت خودکار yourdomain.com در نظر گرفته می‌شود.

myorigin
  • توضیح: این پارامتر مشخص می‌کند که ایمیل‌های ارسال شده از کاربران محلی سرور (مثلاً کاربری که از خط فرمان ایمیل می‌فرستد) با چه دامنه‌ای نمایش داده شوند.
  • مقدار پیش‌فرض:$myhostname (یعنی ایمیل کاربر root به صورت root@mail.yourdomain.com ارسال می‌شود).
  • تنظیم رایج: بسیاری از مدیران سیستم، این مقدار را برابر با $mydomain قرار می‌دهند (myorigin = $mydomain) تا ایمیل‌ها تمیزتر و به صورت root@yourdomain.com ارسال شوند.

پارامترهای شبکه و اتصال (Networking)

این پارامترها نحوه تعامل Postfix با شبکه را کنترل می‌کنند.

inet_interfaces
  • توضیح: این پارامتر حیاتی مشخص می‌کند که Postfix روی کدام رابط‌های شبکه (آدرس‌های IP) برای دریافت ایمیل گوش دهد.
  • مقدار پیش‌فرض:all (به تمام آدرس‌های IP عمومی و محلی سرور گوش می‌دهد).
  • تنظیم امن برای ارسال محلی (مانند ادامه فصل):loopback-only (فقط به 127.0.0.1 گوش می‌دهد و سرور را از اینترنت ایزوله می‌کند).

relayhost
  • توضیح: این پارامتر مشخص می‌کند که آیا Postfix باید ایمیل‌ها را مستقیماً به اینترنت ارسال کند یا آن‌ها را به یک سرور SMTP دیگر (یک "رله هوشمند" یا "Smarthost") تحویل دهد.
  • مقدار پیش‌فرض:خالی. وقتی این مقدار خالی است، Postfix خودش با استفاده از DNS، سرور مقصد را پیدا کرده و مستقیماً به آن متصل می‌شود (این همان کاری است که ما در این دوره انجام دادیم).
  • کاربرد دیگر: اگر می‌خواستید تمام ایمیل‌ها را از طریق سرویس دیگری مانند SendGrid یا Mailgun ارسال کنید، آدرس سرور آن‌ها را در این قسمت وارد می‌کردید: relayhost = [smtp.sendgrid.net]:587.

پارامترهای مقصد و اعتماد (Destination & Trust)

این تنظیمات مشخص می‌کنند که Postfix به چه کسانی اعتماد دارد و ایمیل‌های چه دامنه‌هایی را به عنوان مقصد نهایی می‌پذیرد.

mynetworks
  • توضیح: لیستی از آدرس‌های IP یا شبکه‌های "قابل اعتماد" که اجازه دارند بدون نیاز به احراز هویت، از طریق این سرور ایمیل ارسال (Relay) کنند.
  • مقدار پیش‌فرض: معمولاً شامل آدرس‌های loopback (127.0.0.0/8, [::1]/128) و گاهی زیرشبکه محلی سرور است.
  • نکته امنیتی:هرگز نباید 0.0.0.0/0 را به این لیست اضافه کنید، زیرا سرور شما را به یک Open Relay تبدیل کرده و به سرعت توسط اسپمرها مورد سوءاستفاده قرار می‌گیرد.

mydestination
  • توضیح: این پارامتر لیستی از دامنه‌هایی را مشخص می‌کند که این سرور ایمیل خود را مقصد نهایی آن‌ها می‌داند. اگر ایمیلی برای یکی از این دامنه‌ها دریافت شود، Postfix تلاش می‌کند آن را به صورت محلی (مثلاً به یک صندوق پستی کاربر لینوکس) تحویل دهد.
  • مقدار پیش‌فرض:$myhostname, localhost.$mydomain, localhost.
  • نکته: برای یک سرور "فقط ارسال"، مقادیر پیش‌فرض کاملاً کافی هستند. شما تنها زمانی نام دامنه اصلی (yourdomain.com) را به این لیست اضافه می‌کنید که بخواهید یک سرور ایمیل کامل برای دریافت ایمیل‌های آن دامنه راه‌اندازی کنید.


تحلیل پیکربندی امنیتی Postfix (چرا این روش امن است؟)

inet_interfaces = loopback-only
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
قبل از نوشتن کد، بیایید نگاهی دقیق‌تر به چند پارامتر کلیدی در فایل main.cf تنظیمات Postfix بیندازیم که شما تنظیم کرده‌اید. درک این تنظیمات به ما نشان می‌دهد که چرا سرور ما امن است و فقط به برنامه‌های محلی اجازه ارسال ایمیل می‌دهد.

inet_interfaces = loopback-only
  • این مهم‌ترین پارامتر امنیتی در سناریوی ماست. این دستور به Postfix می‌گوید که فقط و فقط به درخواست‌هایی که از طریق رابط شبکه محلی (loopback interface یا 127.0.0.1) ارسال می‌شوند، گوش دهد. این یعنی سرور Postfix شما از اینترنت عمومی یا حتی از کامپیوترهای دیگر در شبکه محلی شما قابل دسترس نیست. مانند یک خط تلفن داخلی است که فقط از داخل ساختمان می‌توان با آن تماس گرفت.

mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
  • این پارامتر مشخص می‌کند که کدام شبکه‌ها "خودی" یا "مورد اعتماد" (Trusted) هستند. در اینجا، شما فقط شبکه‌های loopback (هم برای IPv4 و هم IPv6) را تعریف کرده‌اید. Postfix به درخواست‌های آمده از این شبکه‌ها اعتماد کامل دارد.

smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
  • این پارامتر، قوانین ارسال (Relay) را مشخص می‌کند. این خط به این صورت خوانده می‌شود:
  • permit_mynetworks: اگر درخواستی از یک شبکه "خودی" (که در mynetworks تعریف شده) آمد، به آن اجازه ارسال ایمیل به هر مقصدی را بده.
  • permit_sasl_authenticated: اگر کاربری احراز هویت کرده باشد، به او اجازه ارسال بده (در سناریوی ما کاربردی ندارد چون احراز هویت را فعال نکرده‌ایم).
  • defer_unauth_destination: اگر هیچ‌کدام از شرایط بالا برقرار نبود، درخواست را رد کن.

نتیجه‌گیری تحلیل: با ترکیب این سه پارامتر، ما یک "قلعه" امن ساخته‌ایم. Postfix فقط به درخواست‌های برنامه‌هایی که روی همان سرور اجرا می‌شوند پاسخ می‌دهد و به آن‌ها اجازه ارسال ایمیل به دنیای خارج را می‌دهد، اما درِ خود را به روی هرگونه درخواست از اینترنت عمومی بسته است. به همین دلیل، در کد #C خود از IP 127.0.0.1 و پورت استاندارد و غیررمزنگاری شده 25 استفاده خواهیم کرد.


آماده‌سازی پروژه #C با MailKit

ابتدا، یک پروژه ساده #C را ایجاد کرده و کتابخانه MailKit را به آن اضافه می‌کنیم. یک پوشه جدید برای پروژه خود بسازید و با ترمینال وارد آن شوید. یک پروژه جدید از نوع "کنسول" ایجاد کنید:
dotnet new console
بسته MailKit را به پروژه اضافه کنید:
dotnet add package MailKit

اولین تلاش برای ارسال و خطای SslHandshakeException

حالا بیایید کد ارسال ایمیل را بنویسیم. در فایل Program.cs، کد زیر را برای یک تست اولیه قرار می‌دهیم.
using MailKit.Net.Smtp;
using MimeKit;

Console.WriteLine("Attempting to send an email...");

// ایجاد پیام
var message = new MimeMessage();
message.From.Add(new MailboxAddress("System Reporter", "reporter@dntips.ir"));
message.To.Add(new MailboxAddress("Admin", "admin-email@example.com")); // آدرس گیرنده را تغییر دهید
message.Subject = "A test email from our C# App";
message.Body = new TextPart("plain")
{
    Text = @"Hello,

This is a test message sent from our secure local Postfix server.

Regards,
C# App"
};

// ارسال پیام
using var client = new SmtpClient();

try
{
    // اتصال به سرور محلی در پورت 25
    await client.ConnectAsync("127.0.0.1", 25, false); // اینجا ممکن است خطا رخ دهد

    Console.WriteLine("Connected to server...");

    await client.SendAsync(message);
    Console.WriteLine("Email sent successfully!");

    await client.DisconnectAsync(true);
}
catch (Exception ex)
{
    Console.WriteLine("An error occurred!");
    Console.WriteLine(ex.ToString());
}
اجرای کد و مشاهده خطا:
پس از اجرای این کد، با خطای زیر مواجه می‌شویم:
Unhandled exception. MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection.
The host name (127.0.0.1) did not match the name given in the server's SSL certificate (email.dntips.ir).
---> System.Security.Authentication.AuthenticationException: The remote certificate was rejected by the provided RemoteCertificateValidationCallback.
چرا این خطا رخ می‌دهد؟
کتابخانه MailKit به صورت پیش‌فرض بسیار امن طراحی شده است. حتی اگر شما useSsl را false قرار دهید، MailKit پس از اتصال به پورت ۲۵، سعی می‌کند با استفاده از دستوری به نام STARTTLS، اتصال را به یک کانال امن و رمزنگاری شده ارتقا دهد. در این لحظه:
  • Postfix گواهی SSL خود را (که برای دامنه email.dntips.ir صادر شده) به MailKit ارائه می‌دهد.
  • MailKit می‌بیند که شما به 127.0.0.1 متصل شده‌اید، اما گواهی برای email.dntips.ir است.
  • چون نام‌ها تطابق ندارند، MailKit به درستی و به دلایل امنیتی، اتصال را قطع می‌کند.

کد نهایی و اصلاح شده برای ارتباط محلی و غیررمزنگاری شده

از آنجایی که ما ۱۰۰٪ مطمئن هستیم که این ارتباط کاملاً داخلی و امن است، می‌توانیم این بررسی‌های امنیتی را برای این اتصال خاص غیرفعال کنیم. کد اصلاح‌شده در فایل Program.cs به شکل زیر خواهد بود:
using MailKit.Net.Smtp;
using MimeKit;

Console.WriteLine("Attempting to send an email...");

var message = new MimeMessage();
message.From.Add(new MailboxAddress("System Reporter", "reporter@dntips.ir"));
message.To.Add(new MailboxAddress("Admin", "admin-email@example.com"));
message.Subject = "A test email from our C# App";
message.Body = new TextPart("plain")
{
    Text = @"Hello,

This is a test message sent from our secure local Postfix server.

Regards,
C# App"
};

using var client = new SmtpClient();

try
{
    // >>>>> شروع تغییرات <<<<<
    // به دلیل اینکه ارتباط کاملا لوکال و مورد اعتماد است،
    // ما اعتبارسنجی گواهی SSL سرور را غیرفعال می‌کنیم.
    client.ServerCertificateValidationCallback = (s, c, h, e) => true;
    client.CheckCertificateRevocation = false;
    // >>>>> پایان تغییرات <<<<<

    // در اینجا صراحتا اعلام می‌کنیم که از SSL استفاده نشود.
    await client.ConnectAsync("127.0.0.1", 25, false);

    Console.WriteLine("Connected to server...");

    await client.SendAsync(message);
    Console.WriteLine("Email sent successfully!");

    await client.DisconnectAsync(true);
}
catch (Exception ex)
{
    Console.WriteLine("An error occurred!");
    Console.WriteLine(ex.ToString());
}
توضیح کد اصلاح شده:
  • client.ServerCertificateValidationCallback = (s, c, h, e) => true;: این خط به MailKit می‌گوید: "مهم نیست گواهی SSL سرور چه بود، چه مشکلی داشت، یا با چه نامی صادر شده بود؛ آن را همیشه قبول کن."
  • client.CheckCertificateRevocation = false;: این خط، بررسی ابطال گواهی را نیز غیرفعال می‌کند تا فرآیند اعتبارسنجی سریع‌تر و ساده‌تر شود.
  • await client.ConnectAsync("127.0.0.1", 25, false);: ما همچنان به MailKit می‌گوییم که از ابتدا با SSL شروع نکند.
با اجرای این کد اصلاح‌شده، برنامه شما با موفقیت به Postfix متصل شده و ایمیل را ارسال خواهد کرد. می‌توانید با دستور tail -f /var/log/mail.log در ترمینال دیگر، لاگ ارسال موفقیت‌آمیز ایمیل را مشاهده کنید.

نکات تکمیلی و مهم

  • هشدار امنیتی بسیار مهم: غیرفعال کردن اعتبارسنجی گواهی SSL (ServerCertificateValidationCallback) یک اقدام بسیار خطرناک در هنگام اتصال به سرورهای عمومی در اینترنت است و شما را در معرض حملات "مرد میانی" (Man-in-the-Middle) قرار می‌دهد. ما فقط و فقط به این دلیل این کار را انجام دادیم که ۱۰۰٪ مطمئن هستیم ارتباط ما با 127.0.0.1 یک کانال بسته، داخلی و مورد اعتماد است. هرگز این کد را برای اتصال به یک سرور SMTP خارجی (مانند smtp.gmail.com) استفاده نکنید.
  • مدیریت پیکربندی: در یک برنامه واقعی، بهتر است آدرس سرور، پورت، و آدرس‌های فرستنده/گیرنده را در یک فایل پیکربندی مانند appsettings.json قرار دهید و آن‌ها را در کد خود به صورت ثابت (Hardcode) ننویسید.
  • مدیریت خطا: بلوک try-catch که در کد قرار دادیم برای شروع خوب است، اما در محیط عملیاتی، باید خطاهای خاص‌تری مانند SmtpCommandException را مدیریت کنید تا بتوانید دلیل دقیق شکست در ارسال را متوجه شوید.