首頁

目前文章總數:172 篇

  

最後更新:2025年 03月 22日

0091. 如何在 Email 內嵌圖片?避免外部圖片 URL 失效的最佳做法

日期:2025年 04月 19日

標籤: Asp.NET Core Web MVC Visual Studio C#

摘要:C# 學習筆記


應用所需:Visual Studio 2022 C#
解決問題:如何避免發送出去的郵件未來 Url 失效時無法檢視,啟用內嵌附件解決此問題
相關參考:0026. log4net 發送Email的方法,使用Gmail為範例
範例專案:範例代碼
基本介紹:本篇分為 4 部分。
第一部分:問題描述
第二部分:Web專案架構
第三部分:代碼說明
第四部分:DEMO 成果






第一部分:問題描述

Step 1:問題說明

查詢自己的 Email 可能會出現如下叉燒包的圖片,這是因為裡面用的是 URL 連結
若想要讓自己的 Image Url 伺服器變更後,不影響原本已送出的 Email 內容,可以採用內嵌附件的方法解決。


第二部分:Web專案架構

Step 1:範例專案架構

打開範例代碼後,架構基本分成以下:

1. Model 檢視模型、API介接 Data Transfer object
2. Service 發送郵件的方法
3. Web控制器 提供發送郵件的頁面檢視、API 發送接口
4. Html 畫面 Html 使用者操作畫面
5. 初始化配置 基本的依賴注入




第三部分:代碼說明

Step 1: Model

用於 API 傳遞發送 Mail 所需參數

public class EmailDTO
{
    public string SmtpServer { get; set; }
    public int SmtpPort { get; set; }
    public string SenderEmail { get; set; }
    public string SenderPassword { get; set; }
    public string RecipientEmail { get; set; }
    public string Subject { get; set; }
}


以及畫面上 FromBody 對應的參數

public class EmailViewModel
{
    [Required(ErrorMessage = "請輸入 SMTP 伺服器")]
    public string SmtpServer { get; set; }

    [Required(ErrorMessage = "請輸入 SMTP 連接埠")]
    [Range(1, 65535, ErrorMessage = "連接埠必須介於 1-65535 之間")]
    public int SmtpPort { get; set; }

    [Required(ErrorMessage = "請輸入寄件者信箱")]
    [EmailAddress(ErrorMessage = "請輸入有效的電子郵件地址")]
    public string SenderEmail { get; set; }

    [Required(ErrorMessage = "請輸入寄件者密碼")]
    public string SenderPassword { get; set; }

    [Required(ErrorMessage = "請輸入收件者信箱")]
    [EmailAddress(ErrorMessage = "請輸入有效的電子郵件地址")]
    public string RecipientEmail { get; set; }

    [Required(ErrorMessage = "請輸入郵件主旨")]
    public string Subject { get; set; }
}



Step 2:Service

代碼依序有 4 個步驟,其中 2-4. 會將取得的圖片轉換為 CID ,在 Email 中以附件檔案的方式提供 Body 呼叫

public class SendEmailService : ISendEmailService
{
    /// <summary>
    /// 使用附件方式功能,發送郵件
    /// </summary>
    public async Task<string> SendEmail(EmailDTO emailDto)
        {
            try
            {
                // 1. 輸入自己的信箱密碼 - 這個要輸入自己安全應用程式上的產生密碼
                string senderPassword = emailDto.SenderPassword;

                // 2-1. 建立 MailMessage
                MailMessage mail = new MailMessage
                {
                    From = new MailAddress(emailDto.SenderEmail),
                    Subject = "個人資料 - 附帶圖片",
                    IsBodyHtml = true,
                };
                // 2-2. 郵件副本對象
                mail.To.Add(emailDto.RecipientEmail);

                // 2-3. 可將圖片網址做為傳參
                var imageBytes = await DownloadImageAsync();

                // 2-4. 將圖片存成附件,讓 Mail 中,不依賴 Url 而是存在 Mail 中
                //      優點:未來Url失效時,此郵件仍可檢閱圖片

                var imageStream = new MemoryStream(imageBytes);
                var inlineImage = new Attachment(imageStream, "louis.jpg", "image/jpeg");

                // 關鍵:設定,並讓這個 Content-ID 用於 HTML 內嵌
                inlineImage.ContentId = "MyImage";
                inlineImage.ContentDisposition.Inline = true;
                inlineImage.ContentDisposition.DispositionType = DispositionTypeNames.Inline;
                mail.Attachments.Add(inlineImage);

                // 2-5. 撰寫 HTML 內容,並且使用 <img> 內嵌圖片
                mail.Body = @"
                <html>
                <body>
                    <p>Dear 先生:</p>
                    <p>你好,我是 XXX (這邊附上 Attachments 圖片)</p>
                    <img src=""cid:MyImage"" width=""300"" alt=""個人照片"" />
                    <p>這是我的個人資料 ....</p>
                    <br />
                    <p>Thanks, have a good day.</p>
                </body>
                </html>";

                // 3. 設定 SMTP 用戶端
                SmtpClient smtpClient = new SmtpClient(emailDto.SmtpServer, emailDto.SmtpPort)
                {
                    Credentials = new NetworkCredential(emailDto.SenderEmail, senderPassword),
                    EnableSsl = true
                };

                // 4. 發送郵件
                smtpClient.Send(mail);
                return "郵件發送成功!";
            }
            catch (Exception ex)
            {
                return $@"郵件發送失敗:{ex.Message}";
            }

        }

    /// <summary>
    /// 下載網路圖片
    /// </summary>
    private static async Task<byte[]> DownloadImageAsync(
        string url = "https://gotoa1234.github.io/assets/image/ContinuousDeployment/docker/2025_03_08/005.png")
    {
        using (HttpClient client = new HttpClient())
            {
                try
                {
                    var getResult = await client.GetByteArrayAsync(url);

                    if (getResult == null ||
                        getResult.Length == 0)
                    {
                        throw new Exception("圖片下載失敗!");
                    }
                    return getResult;
                }
                catch (Exception ex)
                {
                    throw new Exception("圖片下載錯誤:" + ex.Message);
                }
            }
    }
}



Step 3:Web控制器

控制器只存在 2 個功能 : 檢視與API

public IActionResult Index()
{
    return View();
}

[HttpPost]
public async Task<IActionResult> SendEmail([FromBody] EmailDTO model)
{
    try
    {
        var result = await _sendEmailService.SendEmail(model);
        return Ok(
            new { success = true, message = result });
    }
    catch (Exception ex)
    {
        return BadRequest(new { success = false, message = ex.Message });
    }
}



Step 4:Html 畫面

發送郵件所需的 6 項參數,為必填資料,否則無法成功透過程式發送 Email

@model SendGoogleEmailCIDWithAttachementsExample.Models.EmailViewModel
@{
    ViewData["Title"] = "發送郵件範例";
}

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h3 class="text-center">發送郵件</h3>
                </div>
                <div class="card-body">
                    <form id="emailForm">
                        <div class="mb-3">
                            <label for="smtpServer" class="form-label">SMTP 伺服器</label>
                            <input type="text" class="form-control" id="smtpServer" name="SmtpServer" value="smtp.gmail.com" required>
                        </div>
                        <div class="mb-3">
                            <label for="smtpPort" class="form-label">SMTP 連接埠</label>
                            <input type="number" class="form-control" id="smtpPort" name="SmtpPort" value="587" required>
                            <small class="text-muted">常用連接埠: 465 (SSL) / 587 (TLS)</small>
                        </div>
                        <div class="mb-3">
                            <label for="senderEmail" class="form-label">寄件者信箱</label>
                            <input type="email" class="form-control" id="senderEmail" name="SenderEmail" value="cap8826@gmail.com" required>
                        </div>
                        <div class="mb-3">
                            <label for="senderPassword" class="form-label">寄件者密碼</label>
                            <input type="password" class="form-control" id="senderPassword" name="SenderPassword" value="1234567890" required>
                        </div>
                        <div class="mb-3">
                            <label for="recipientEmail" class="form-label">收件者信箱</label>
                            <input type="email" class="form-control" id="recipientEmail" name="RecipientEmail" value="cap8825@gmail.com" required>
                        </div>
                        <div class="mb-3">
                            <label for="subject" class="form-label">郵件主旨</label>
                            <input type="text" class="form-control" id="subject" value="個人資料 - 附帶圖片" name="Subject" required>
                        </div>
                        <div class="d-grid">
                            <button type="button" id="sendButton" class="btn btn-primary">發送郵件</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 顯示結果的模態框 -->
<div class="modal fade" id="resultModal" tabindex="-1" aria-labelledby="resultModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="resultModalLabel">郵件發送結果</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body" id="resultMessage">
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">關閉</button>
            </div>
        </div>
    </div>
</div>

@section Scripts {
    <script>
        $(document).ready(function () {
            $('#sendButton').on('click', function () {
                // 顯示載入中的按鈕狀態
                const button = $(this);
                const originalText = button.text();
                button.prop('disabled', true);
                button.html('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 處理中...');

                // 獲取表單數據
                const formData = {
                    smtpServer: $('#smtpServer').val(),
                    smtpPort: parseInt($('#smtpPort').val()),
                    senderEmail: $('#senderEmail').val(),
                    senderPassword: $('#senderPassword').val(),
                    recipientEmail: $('#recipientEmail').val(),
                    subject: $('#subject').val(),
                    body: $('#body').val()
                };

                // 呼叫後端 API
                $.ajax({
                    url: '/Home/SendEmail',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify(formData),
                    success: function (response) {
                        $('#resultMessage').html('<div class="alert alert-success">郵件發送成功!</div>');
                        $('#resultModal').modal('show');
                        // 清空表單的主旨和內容
                        $('#subject').val('');
                        $('#body').val('');
                    },
                    error: function (xhr, status, error) {
                        let errorMessage = '郵件發送失敗。';
                        if (xhr.responseJSON && xhr.responseJSON.message) {
                            errorMessage += '<br>詳細錯誤: ' + xhr.responseJSON.message;
                        }
                        $('#resultMessage').html('<div class="alert alert-danger">' + errorMessage + '</div>');
                        $('#resultModal').modal('show');
                    },
                    complete: function () {
                        // 恢復按鈕狀態
                        button.prop('disabled', false);
                        button.text(originalText);
                    }
                });
            });
        });
    </script>
}



Step 5:初始化配置

在 Asp.net Core 中添加以下注入:

builder.Services.AddScoped<ISendEmailService, SendEmailService>();



第四部分:DEMO 成果

Step 1:啟用應用程式密碼

若要從程式中發送自己的 Google Email ,需要啟用應用程式密碼配置
也可參考此篇:0026. log4net 發送Email的方法,使用Gmail為範例


Step 2:啟用應用程式密碼 - 建立

輸入一個名稱,自己知道用途,後續不使用可以進行刪除


Step 3:啟用應用程式密碼 - 產生密碼

系統會為您的 Google 帳戶產生一組密碼,此密碼可以用於程式碼的發送 Email 郵件


Step 4:DEMO成果 - 啟動程式碼

範例代碼啟動後,輸入相關資訊,並且發送


Step 5:DEMO成果 - 完成

產生的 Email 中,可以正確的使用 Email 中的附件圖片,而非 URL,未來即使自己架設的 Image Server 變動,也不會影響已發送的 Email