Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for adding attachments as LinkedResources in MailMessage #376

Open
johnwc opened this issue Feb 27, 2024 · 6 comments
Open

Add support for adding attachments as LinkedResources in MailMessage #376

johnwc opened this issue Feb 27, 2024 · 6 comments

Comments

@johnwc
Copy link

johnwc commented Feb 27, 2024

Describe the solution you'd like
We need to be able to add attachments that are considered LinkedResources to the MailMessage object, so the attached/embedded images can be used within the html with their ContentId. I will create a PR for this update.

@jamesmh Within this PR, is it ok if I add a new property to the Attachment class that allows using a Stream? So that a developer can pass a byte array or a Stream.

@johnwc
Copy link
Author

johnwc commented Mar 8, 2024

@jamesmh thoughts?

@johnwc
Copy link
Author

johnwc commented Sep 20, 2024

@jamesmh did this make it into the v6?

@jamesmh
Copy link
Owner

jamesmh commented Sep 20, 2024

Hey, haven't taken a look at this. If you want to give it a try feel free. If we are going to go with something that for now doesn't change the existing API surface as much as possible, then maybe the usage would be something like:

public class NewUserViewMail : Mailable<UserModel>
{
    private UserModel _user;
    private MemoryStream _image;

    public NewUserViewMail(UserModel user, MemoryStream image)
    {
        this._user = user;
        this._image = image;
    }

    public override void Build()
    {
        this.To(this._user)
            .From("from@test.com")
            .AddLinkedResource(_image, "image_id") // Signature is `AddLinkedResource(Stream, string)`
            .View("~/Views/Mail/NewUser.cshtml", this._user);
    }
}

So we would have two variations of the method:

AddLinkedResource(string filePath, string contentId);
AddLinkedResource(Stream fileStream, string contentId);

With this approach, the html will already have set static contentIds for each image, which are then also set using AddLinkedResource().

Internally, the Mailer class would be very similar to what exists for the attachments logic. Like in the SmptMailer we'd add the logic to add the image to the LinkedResources in SetMailBody() starting at line 95/96.

If that makes sense, feel free to add one or both of those signatures. Any other ideas welcome too 👍

@johnwc
Copy link
Author

johnwc commented Sep 20, 2024

We, actually already have it working great. We took your existing SmtpMailer and created our own with the modification. We could not change the Attachment object to include the needed ContentId field, so we utilized the attachments Name filed to check if it started with cid:.

But, this could be done much simpler by only adding a new ContentId property to the Attachment object. And then using it conditionally in the SetMailBody method.

private static void SetMailBody(string message, IEnumerable<Attachment> attachments, MimeMessage mail)
{
    var bodyBuilder = new BodyBuilder { HtmlBody = message };
    if (attachments != null)
    {
        foreach (var attachment in attachments)
        {
            if (string.IsNullOrWhiteSpace(attachment.ContentId) == false)
            {
                var image = bodyBuilder.LinkedResources.Add(attachment.Name, attachment.Bytes);
                // We do this instead of using their value because RFC states the Content-Id value must be in the message id format.
                image.ContentId = MimeKit.Utils.MimeUtils.GenerateMessageId();
                // Now replace where they applied it in the html template with the updated correct version
                bodyBuilder.HtmlBody = bodyBuilder.HtmlBody.Replace($"\"cid:{attachment.ContentId}\"", $"\"cid:{image.ContentId}\"");
            }
            else
                bodyBuilder.Attachments.Add(attachment.Name, attachment.Bytes);
        }

    }

    mail.Body = bodyBuilder.ToMessageBody();
}

Let me know if you want a PR for this, and I will make the updates with the overload with the Stream type with it.

@jamesmh
Copy link
Owner

jamesmh commented Sep 24, 2024

That's a pretty cool approach I think @johnwc 😄 If you can make a PR scoped to this specific change I think that would be awesome👍

@johnwc
Copy link
Author

johnwc commented Oct 20, 2024

@jamesmh I just create the PR for this. #413

If you are ok with it, I would like to add another small update. In our deployments, we make sure that all emails going out of development environments are prefixed with Dev: . With this small update shown, we can inherit from the OoB SmtpMailer, and make our needed update for the subject when we detect we're deployed to dev.

public class SmtpMailer : IMailer
{
    ...
    public virtual Task BeforeSend(MimeMessage mail)
    {
        return Task.CompletedTask;
    }

    public async Task SendAsync(MessageBody message, string subject, IEnumerable<MailRecipient> to, MailRecipient from, MailRecipient replyTo, IEnumerable<MailRecipient> cc, IEnumerable<MailRecipient> bcc, IEnumerable<Attachment> attachments = null, MailRecipient sender = null)
    {
        var mail = new MimeMessage();
            
        this.SetFrom(@from, mail);
        SetSender(sender, mail);
        SetRecipients(to, mail);
        SetCc(cc, mail);
        SetBcc(bcc, mail);
        mail.Subject = subject;
        SetMailBody(message, attachments, mail);

        if (replyTo != null)
        {
            SetReplyTo(replyTo, mail);
        }

        using (var client = new SmtpClient())
        {
            client.ServerCertificateValidationCallback = this._certCallback;
            await client.ConnectAsync(this._host, this._port).ConfigureAwait(false);

            if (this.UseSMTPAuthentication())
            {
                await client.AuthenticateAsync(this._username, this._password);
            }

            await BeforeSend(mail).ConfigureAwait(false);
            await client.SendAsync(mail).ConfigureAwait(false);
            await client.DisconnectAsync(true).ConfigureAwait(false);
        }
    }
    ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants