將資料存成eml格式下載-使用裝飾模式

之前曾經有個案例是要將使用者輸入的文案轉存成 EMAIL 的格式,下載後可以讓他們自行編輯再轉發出去,這邊就記錄一下重點

概略說明

因為先前採用的輸入介面是 CKEditor , 然後使用者如果在裡面編輯的話,實際上是存成 HTML,但如果它們又想要轉寄這些信件,CKEditor 的所見即所得就會造成很大的困擾。因為並不是每一家的 EMAIL 服務都能夠支援這些 HTML 語法與 CSS,大都是只有支援一部分,有許多新的語法並不支援。但是使用者又希望它們能夠在網站上直接將這些東西轉寄出去,當然如果直接這樣幹的話會有很多繞不過去的問題。

最終的解決方案就是將這些東西轉存成 eml 格式,然後讓使用者下載下來,讓它們自己編輯後再轉寄出去
附件的部分直接在編輯時附加在信件內;圖片則是轉 base64 後就可以直接顯示

  1. 前端就是點擊後呼叫後端並下載存成 EML 檔案
  2. 後端就是負責提供某個文章的 EMAIL 格式資料給前端

下載檔案

前端的部分比較單純,呼叫後端後將內容存檔即可

1
2
3
4
5
6
7
8
9
10
11
12
13
async mailDownload(id) {
// 向後端發出請求取得檔案內容
let response = await ArticleModule.downloadEmail(id)
// 檔案下載
const $a = document.createElement("a")
const url = URL.createObjectURL(response.data)
const fileName = response.headers["content-disposition"].split("=")[1]
const currentFileName = fileName.replace(/"(.*)"/, "$1").replace("UTF-8''", "")
$a.download = decodeURIComponent(currentFileName)
$a.href = url
$a.click()
setTimeout(() => URL.revokeObjectURL(url), 5000)
}

轉存成 eml 格式

透過.EML - Email Generation in JS範例,可以知道核心的解決方案就是像範例中一樣,把一些郵件的資訊放在最前面,指定好收件人、信件主旨,接著就是準備 HTML 的信件內容

又因為這邊要做兩三件事情,都是針對信件內容作加工處理,我覺得還蠻適合用裝飾模式,就順便練習一下

  1. 針對信件內容的圖片網址,轉 base64 編碼
  2. 針對信件內容,最前面加上一些信件的資訊,例如收件人、寄件人、信件主旨等等
  3. 針對信件內容的部分,將 CKEditor 的 CSS 附加上去

裝飾模式

大概就跟穿衣服一樣,所有穿衣服的動作都是圍繞著人這個主體,概念就是這樣而已,穿衣服的行為就是在人的身上穿衣服、穿褲子、穿內衣、戴手錶、穿襪子、穿鞋子這些事情而已,所以就是用下面的介面來表示這個行為

1
2
3
4
public interface IPeopleDecorator
{
string Decorate(string people);
}

用這個例子來說的話,裝飾前是:,裝飾後就會是:穿著運動上衣的人,其他的裝飾也是一樣的概念

出門運動跑步,大概就會知道你要在你的身上弄好:衣服、褲子、鞋子,最後可能就會穿著運動上衣、運動褲、運動鞋的人
出門上班上課,大概就會知道你在身上要弄好的是:衣服、褲子、鞋子、背包,最後可能就會穿著襯衫、西裝褲、皮鞋、還拿個公事包的人

這只是個例子,不要太認真要帶什麼東西

所以用上面的概念,你就會知道有一個 人的裝飾工廠,可能有兩個方法,一個是準備出門運動的行頭 method、另外一個是準備出門上班上課的行頭 method

like this

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

public static class PeopleDecoratorFactory
{
public static IPeopleDecorator Sport()
{
// 運動上衣
// 運動褲
// 運動鞋
return new PeopleDecorator(運動上衣,運動褲,運動鞋);
}

public static IPeopleDecorator Work()
{
// 襯衫
// 西裝褲
// 皮鞋
// 公事包
return new PeopleDecorator(襯衫,西裝褲,皮鞋,公事包);
}
}

```
`PeopleDecorator`就是實際上穿衣服的行為,用程式碼的概念就是下面這樣

```cs
internal class PeopleDecorator : IPeopleDecorator
{
private readonly List<IPeopleDecorator> _decorators;

public ContentDecorator(params IPeopleDecorator[] decorators)
{
// 透過建構式把剛剛要準備裝飾的東西都放進來
_decorators = decorators.ToList();
}


// 好吧,下面這段我覺得如果改成中文的人,反而更難懂
// 反正概念就是把剛剛準備要裝飾的東西,一個一個的裝飾在人的身上
// 就等同於我們把要裝飾的項目,一個一個的在內容上加工是一樣的道理
public string Decorate(string content)
{
string decoratedContent = content;
foreach (var decorator in _decorators)
{
decoratedContent = decorator.Decorate(decoratedContent);
}

return decoratedContent;
}
}

最終要使用的時候就是向下面這樣呼叫就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public string GoToPark()
{
var people = "art";
var decorator = PeopleDecoratorFactory.Sport();
var result = decorator.Decorate(people);
return result;
}

public string GoToWork()
{
var people = "art";
var decorator = PeopleDecoratorFactory.Work();
var result = decorator.Decorate(people);
return result;
}

整體的概念及程式碼就是這樣了,下面用實際的範例來感覺一下

實際範例

輸入驗證的部分以及例外處理的部分因為不是本文重點,直接略過,若實際要用的話記得補上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Controller
public FileResult DownloadEmail(int id)
{
// 取得文章內容
var article = ArticleModule.Get(id);

// 準備好裝飾者
var decorator = ContentDecoratorFactory.CreateDecorator(article);

// 將裝飾者加工後的內容以UTF8編碼轉成byte陣列
var emailContent = decorator.Decorate(article.Content);
var fileBytes = Encoding.UTF8.GetBytes(emailContent);

// 指定檔案名稱並回傳
var fileName = $"[{article.Type.GetDescription()}] {article.Subject}.eml";
return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static class ContentDecoratorFactory
{
// 規定好的圖片網址格式
private const string ImageUrlPattern = @"\/Common\/ArticleImage\/[a-zA-Z0-9]{32}";

public static IContentDecorator CreateDecorator(ArticleEntity article)
{
var pattern = new Regex(ImageUrlPattern);
var imageUrlDecorator = new ArticleImageDecorator(pattern);
var styleDecorator = new CkeditorStyleDecorator();
var htmlDecorator = new HtmlTagDecorator(article.Subject);

return new ContentDecorator(imageUrlDecorator, styleDecorator, htmlDecorator);
}
}
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
internal class articleImageDecorator : IContentDecorator
{
private readonly Regex _pattern;
private IarticleFileInfoDao _articleFileInfoDao;
private IarticleFileInfoDao articleFileInfoDao => _articleFileInfoDao ?? (_articleFileInfoDao = DataFactory.GetarticleImageInfoDAO());

public articleImageDecorator(Regex pattern)
{
_pattern = pattern;
}

public string Decorate(string content)
{
return _pattern.Replace(content, match =>
{
// 取得圖片的 GUID
var guid = match.Value.Split('/').Last();
// 依據 GUID 找實體檔案資料
articleFileInfo fileInfo = articleFileInfoDao.GetFileInfoByGuid(guid);
if (fileInfo == null) throw new MyException(ExceptionCode.NotFound, $"cannot found fileInfo by Guid:{match.Value}");
// 檔案實際儲存的路徑
var baseFolder = "D:\\article\\"
// 讀取檔案
var filePath = string.Concat($@"{baseFolder}{fileInfo.articleId}\", $"{guid}.jpg");
byte[] imageBytes = File.ReadAllBytes(filePath);
string base64String = Convert.ToBase64String(imageBytes);

return $"data:image/jpeg;base64,{base64String}";
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
internal class CkeditorStyleDecorator : IContentDecorator
{
public string Decorate(string content)
{
var sb = new StringBuilder();
sb.AppendLine("<style>");
// 把 CKEditor CSS 放在這邊,當然也可以選擇不要做這件事情,這邊只是範例
// sb.AppendLine("@".ck.ck-placeholder:before,.ck .ck......")
sb.AppendLine("</style>");
sb.AppendLine(content);
return sb.ToString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
internal class HtmlTagDecorator : IContentDecorator
{
private readonly string _subject;

public HtmlTagDecorator(string subject)
{
_subject = subject;
}

public string Decorate(string content)
{
var sb = new StringBuilder();
sb.AppendLine("To:");
sb.AppendLine("Subject: " + _subject);
sb.AppendLine("X-Unsent: 1");
sb.AppendLine("Content-Type: text/html; charset=utf-8");
sb.AppendLine("");
sb.AppendLine(@"<html><head></head><body>");
sb.AppendLine(content);
sb.AppendLine("</body></html>");
return sb.ToString();
}
}

結論

整體上就是這樣子,看程式碼可能會有點難理解,不過如果概念懂了之後,用什麼辦法都沒關係,程式碼只是輔助而已,重點是能不能做到你想要的事情,這才是重點。關於裝飾模式有很多介紹的文章跟書籍都有談到,我也看了好多個,以前還寫了個 js 版本的JavaScript Decorator Pattern,最後還是因為這個工作上實際應用上了,才比較有感覺,所以這邊就記錄一下,希望也對別人有幫助。