0%

[DotnetCore]SMTP寄信服務設計

前提情要

這篇的誕生是因為筆者在工作上專案時有寄信的需求,且會有各式各樣的服務皆可能需要寄信服務,各式各樣服務包含應用程式端、資料庫端像Store Procedure運算完需寄信通知,目標要讓不同服務都有相同的接口去對應。

我們的想法是設計一個資料表,有寄信需求則往該資料表新增一筆資料,再由一個專門pooling的排程服務依照Status,一一的把信件寄出去,由於因為是靠Status在運作,更新Status變成是一個很重要的任務。

大概運作方式就是,排程服務時間到要運行時,會將 Status = 'N' 符合的資料撈出來,一筆一筆進行寄信作業,但由於在foreach中若用同步的方式寄信則會大塞車,當然要考慮最差狀況,因此必須要使用非同步方式進行寄信。但非同步方式的話,只要 SendMailAsync() 後會與主程式脫鉤,無法掌握寄信成功與否,更新其對應的紀錄,將Status更新為YF

內容

筆者第一步是想到SmtpClient本身應該會有提供相對應的事件,查一下就會看到有SendCompleted 可以觸發,但此call back function帶回的參數,為object sender, AsyncCompletedEventArgs e,只知道寄信結果及SmtpClient(Sender)相關資訊,無法取得寄信資料表對應的資料,才有辦法做更新作業。

SmtpClient.SendCompleted 事件 (System.Net.Mail)

筆者接下去思考,自己必須要包一個類別,把寄信資料表對應的資料接進來變內部變數,對外開放Event,使外部使用端可以接受寄信結果完成事件觸發,再由該Event時丟出

  • 寄信資料表對應的資料
  • 寄信作業結果

寄信需求資料

來實作吧,首先先規劃出寄信需求資料結構,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MailToDoItem
{
public int SeqNo { get; set; }
// 服務名稱
public string SourceId { get; set; }
// 主要收信者(多個以;隔開)
public string Mail_To { get; set; }
// 副本收信者(多個以;隔開)
public string Mail_Cc { get; set; }
// 信件主旨
public string Subject { get; set; }
// 信件內容
public string Message { get; set; }
// 寄信狀態(N, P, Y, F)
// N: 尚未寄信
// P: 寄信中
// Y: 寄信完成
// F: 寄信失敗(搭配ErrMessage)
public string Status { get; set; }
// 寄信者
public string SendUser { get; set; }
// 寄信時間
public DateTime? SendTime { get; set; }
// 錯誤訊息
public string ErrMessage { get; set; }
}

寄信服務類別

接著自己的寄信服務類別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class MySmtpClient : IDisposable
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private MailToDoItem _mailToDo;
private SmtpClient _smtpClient;
public event Action<MailToDoItem, AsyncCompletedEventArgs> MySendCompleted;
public SKBSmtpClient(MailToDoItem mailToDoCfm, EmailSetting emailSetting)
{
_mailToDo = mailToDo;
_smtpClient = new SmtpClient(emailSetting.SmtpServer);
_smtpClient.SendCompleted += new SendCompletedEventHandler(_smtpClient_SendCompleted);
}

private void _smtpClient_SendCompleted(object sender, AsyncCompletedEventArgs e)
{
MySendCompleted(_mailToDo, e);
_smtpClient.SendCompleted -= _smtpClient_SendCompleted;
this.Dispose();
}

public void SendMailProcess(string systemId
, byte[] attach, string attachName)
{
MailMessage mailMessage = new MailMessage();

var mailFrom = new MailAddress($"{systemId}@company.com.tw", systemId);
mailMessage.From = mailFrom;
foreach (var item in _mailToDo.Mail_To?.Split(";"))
{
if (string.IsNullOrEmpty(item) == false)
mailMessage.To.Add(item);
}
foreach (var item in _mailToDo.Mail_Cc?.Split(';'))
{
if (string.IsNullOrEmpty(item) == false)
mailMessage.CC.Add(item);
}
mailMessage.Body = _mailToDo.Message;
mailMessage.Subject = _mailToDo.Subject;

if (attach != null && attach?.Length > 0)
{
Attachment attachment = new Attachment(new MemoryStream(attach), attachName);
mailMessage.Attachments.Add(attachment);
}
_smtpClient.SendMailAsync(mailMessage);
}

public void Dispose()
{
_logger.Info($"[MySmtpClient Dispose]!!");
this._smtpClient.Dispose();
// Suppress finalization.
GC.SuppressFinalize(this);
}
}

其中EmailSetting,要再宣告一下,也是從外面傳進去,基本上是寄信相關的設定物件

1
2
3
4
5
6
7
public class EmailSetting
{
public string SmtpServer { get; set; }
public int SmtpPort { get; set; }
public string Account { get; set; }
public string Password { get; set; }
}

以上 MySmtpClient 包裝SmtpClient,達到物件導向設計三大特性中的封裝,你不需要知道我怎麼寄信,把寄信資料表對應的資料SmtpServer相關的資訊給我,我幫你完成寄信,且寄信完成後會再由你自定義的事件觸發讓你知道已完成。

使用端使用方式也很簡單,第一步宣告事件對應的Action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void MailSendCompleted(MailToDoItem mailToDoItem, AsyncCompletedEventArgs e)
{
_logger.Info($"[Send Completed]: {JsonConvert.SerializeObject(mailToDoItem)}");
using (var _conn = new SqlConnection(_connStr))
{
var result = e.Error != null ? "F" : "Y";
var errMsg = e.Error != null ? e.Error.ToString() : "";
_conn.Execute(@"UPDATE MailToDo SET Status = @Status, ErrMessage = @ErrMessage
, SendUser = @SendUser, SendTime = @SendTime WHERE SeqNo = @SeqNo"
, new
{
Status = result,
mailToDoItem.SeqNo,
SendUser = "SYS",
SendTime = DateTime.Now,
ErrMessage = errMsg
});
}
}

寄信Client端

再寄信需求程式片段中呼叫:

1
2
3
var mySmtpClient = new MySmtpClient(mailToDoItem, emailSetting);
mySmtpClient.SKBSendCompleted += MailSendCompleted;
mySmtpClient.SendMailProcess("MySystem.Admin", null, null);

結論

以上,筆者在實際掛排程服務跑過,是沒有問題的,確實感受到非同步的好處,以及 event, action的搭配,堪稱是完美。要記住event, action的搭配,筆者會在爾後會常使用到該技巧。