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


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

概略說明

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

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

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

下載檔案

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

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 附加上去

裝飾模式

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

public interface IPeopleDecorator
{
    string Decorate(string people);
}

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

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

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

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

like this


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;
    }
}

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

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;
}

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

實際範例

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

// 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);
}
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);
    }
}
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}";
        });
    }
}
    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();
        }
    }
    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,最後還是因為這個工作上實際應用上了,才比較有感覺,所以這邊就記錄一下,希望也對別人有幫助。