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

Inline attachments #159

Merged
merged 25 commits into from
Oct 22, 2017
Merged

Inline attachments #159

merged 25 commits into from
Oct 22, 2017

Conversation

theodowling
Copy link
Contributor

@theodowling theodowling commented Oct 16, 2017

Hi there

I needed inline-attachment support for a project, so I added it for myself, and thought it might make a good PR.

Inline-Attachment support has been added to Sendgrid, Mandrill, Mailgun, Postmark and SparkPost adapters.

Update: SMTP adapter also supported

At its core you just need to specify type: :inline when creating an Attachment, and all the rest will happen automatically.

Attachment.new("/data/file.png", type: :inline)

And the ContentID of the attachment will default to filename so in the above case, you can refer to the inline-attached image by using:

<img src="cid:file.png">

I've tested using all of the adapters listed above. Please let me know if you have any comments, or need me to change anything.

Kind regards
Theo

@princemaple
Copy link
Member

Hi Theo,

I'm on my phone.
I just want to quickly say thank you for the great effort!

I've scanned through the PR, it's mostly good. I'll go through it again on my desktop soon. Probably just nits so I might simply merge this and tweak the styles / implementation details myself.

Po

Copy link
Member

@princemaple princemaple left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @theodowling

This is my first round review on the PR.
The comments are for both myself and @stevedomin to think about. You are welcome to address the nitpicks if you'd like to. If not, I'll clean it up before I merge.

@stevedomin the main things I want to talk about are type: :inline|:attachment vs inline: true|false, and whether to special case inline attachments and perform checks on content types, etc.

@theodowling out of curiosity, have you looked into SMTP inline attachments?

@@ -86,10 +88,23 @@ defmodule Swoosh.Adapters.Mailgun do

defp prepare_attachments(body, %{attachments: []}), do: body
defp prepare_attachments(body, %{attachments: attachments}) do
Map.put(body, :attachments, Enum.map(attachments, &prepare_file(&1)))
normal_attachments = Enum.filter(attachments, fn(%Swoosh.Attachment{type: type}) -> type == :attachment end)
inline_attachments = Enum.filter(attachments, fn(%Swoosh.Attachment{type: type}) -> type == :inline end)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Enum.split_with
  • Matching struct type in private functions is not necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Thanks for recommending Enum.split_with. I'll fix this on my side.

{:multipart,
params
|> Map.drop([:attachments])
|> Map.drop([:inline])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map.drop([:attachments, :inline])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'll update this.


inline_attachments = Enum.map(inline_attachments, fn %Swoosh.Attachment{content_type: type, path: path, filename: filename} ->
content = path |> File.read! |> Base.encode64
%{type: type, name: "#{filename}", content: content}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I'd extract this common part into a separate function
  • "#{filename}" is just filename?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has also been updated. Thanks.

@@ -3,7 +3,7 @@ defmodule Swoosh.Attachment do
Struct representing an attachment in an email.
"""

defstruct filename: nil, content_type: nil, path: nil
defstruct filename: nil, content_type: nil, path: nil, type: nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Maybe just inline: false (default to attachment)?
  • Should we do something special about inline attachments? e.g. validating it's an image?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with using type. Slightly more future-proof.

@princemaple: regarding validation of inline attachments, do you know if the spec says anything about accepted file type for inline attachments?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually failed to search for the inline attachment related specs. :(

@@ -464,23 +464,30 @@ defmodule Swoosh.Email do
as an argument. If you give a path we will detect the MIME type and determine the filename
automatically.

You can also send an inline-attachment used for embedding images in the body of emails by specifying `type: "inline"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it's :inline not "inline"
(might change, see comment above)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@theodowling
Copy link
Contributor Author

Thanks @princemaple - I'll wait for you and @stevedomin to discuss the handling of type: :inline|:attachment vs inline: true|false. I myself wasn't sure which approach to take.

I'll also commit some small fixes/changes on the back of your comments above.

I haven't looked at the SMTP implementation, but I can do that also today.

Copy link
Member

@stevedomin stevedomin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theodowling thank you so much for this, great work!

Pointed out a few things in comment but otherwise this is good to go.

@@ -3,7 +3,7 @@ defmodule Swoosh.Attachment do
Struct representing an attachment in an email.
"""

defstruct filename: nil, content_type: nil, path: nil
defstruct filename: nil, content_type: nil, path: nil, type: nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with using type. Slightly more future-proof.

@princemaple: regarding validation of inline attachments, do you know if the spec says anything about accepted file type for inline attachments?

mix.lock Outdated
@@ -1,20 +1,20 @@
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New version of Hex?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More like an older version. @theodowling Please exclude the mix.lock change :) You might want to upgrade with mix local.hex

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads-up. I've reverted change to mix.lock

@@ -464,23 +464,30 @@ defmodule Swoosh.Email do
as an argument. If you give a path we will detect the MIME type and determine the filename
automatically.

You can also send an inline-attachment used for embedding images in the body of emails by specifying `type: "inline"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@theodowling
Copy link
Contributor Author

Thanks for the Approval.
I'm still working on adding inline-attachment support to SMTP adapter. Will push that as soon as I get it working 😄

@princemaple
Copy link
Member

@theodowling thanks for looking into it!

@stevedomin
Copy link
Member

@theodowling nice work on the SMTP attachment!

Copy link
Member

@princemaple princemaple left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @theodowling

Thanks a lot for this PR.
This is my final round of review. It's 100% nitpicks.
Feel free to ignore them if you can't be bothered :) , I'll fix them before/after merge.

@stevedomin a final 👍 ?

@@ -86,13 +88,17 @@ defmodule Swoosh.Adapters.Mailgun do

defp prepare_attachments(body, %{attachments: []}), do: body
defp prepare_attachments(body, %{attachments: attachments}) do
Map.put(body, :attachments, Enum.map(attachments, &prepare_file(&1)))
{normal_attachments, inline_attachments} = Enum.split_with(attachments, fn(%Swoosh.Attachment{type: type}) -> type == :attachment end)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate lines

{..} =
  ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove Swoosh.Attachment and unnecessary braces, fn %{...} -> ... end is sufficient

"content" => &1.path |> File.read! |> Base.encode64
}))
normal_attachments = prepare_attachments_structure(Enum.filter(attachments, fn(attachment) -> attachment.type == :attachment end))
inline_attachments = prepare_attachments_structure(Enum.filter(attachments, fn(attachment) -> attachment.type == :inline end))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enum.split_with (might as well just copy paste the change from mailgun)

end

defp prepare_attachments_structure(attachments) do
Enum.map(attachments, fn %Swoosh.Attachment{content_type: type, path: path, filename: filename} ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove Swoosh.Attachment

"ContentType" => &1.content_type,
"Content" => &1.path |> File.read! |> Base.encode64
}))
Map.put(body, "Attachments", Enum.map(attachments, fn(attachment) -> prepare_attachment(attachment) end))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unnecessary braces in fn arguments

defp prepare_attachment(attachment) do
case attachment.type do
:inline -> %{"Name" => attachment.filename,
"ContentType" => attachment.content_type,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation is off

@@ -106,9 +106,12 @@ defmodule Swoosh.Adapters.Sendgrid do

defp prepare_attachments(body, %{attachments: []}), do: body
defp prepare_attachments(body, %{attachments: attachments}) do
attachments = Enum.map(attachments, fn %{content_type: type, path: path, filename: filename} ->
attachments = Enum.map(attachments, fn %Swoosh.Attachment{content_type: content_type, path: path, filename: filename, type: type} ->
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove Swoosh.Attachment

@@ -57,6 +57,8 @@ defmodule Swoosh.Adapters.SparkPost do
html_body: html,
attachments: attachments
} = email) do
normal_attachments = Enum.filter(attachments, fn(attachment) -> attachment.type == :attachment end)
inline_attachments = Enum.filter(attachments, fn(attachment) -> attachment.type == :inline end)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

split_with

@theodowling
Copy link
Contributor Author

Hi @princemaple, thanks for the comments. I've made the changes as requested.
Please let me know if there is anything else we need to update/change. I'm happy to help out 👍

@stevedomin
Copy link
Member

I'm 👍, feel free to merge if you feel it's good to go @princemaple

@princemaple
Copy link
Member

@theodowling thank you very much for the great work. Merging now!

@princemaple princemaple merged commit e5c2711 into swoosh:master Oct 22, 2017
@blackham
Copy link

blackham commented Apr 30, 2021

You would do well to update your documentation to mention the bit about the cid name being the filename for inline attachments. Even better to provide a simple example. (searching github bugs is a poor replacement for docs)

@princemaple
Copy link
Member

@blackham It's not very helpful to comment on a PR closed over 3 years ago.

It'd be better you filed an issue to tell us what troubled you, and how we can improve. Basically what you did with the comment. Even better you could send a PR to address the issue.

@blackham
Copy link

blackham commented Apr 30, 2021

Sorry it was late. Let me try and explain and give you example code you might be able to use in the doc. Also sorry for commenting on an old bug.

It would have saved me some time last night if the text "Inline attachments by default use their filename (or basename of the path if filename is not specified) as cid, in relevant adapters." found in https://hexdocs.pm/swoosh/Swoosh.Attachment.html also gave a very basic example.

Just this would have been enough for the light bulb to go off.
<img src='cid:image.png'>

Or better, I could have skipped most everything else in the docs and just looked at this example:

   new()
    |> to({user.name, user.email})
    |> from({"Red Skull", "[email protected]"})
    |> subject("OTP QR Code")
    |> html_body("<h1>Hello #{user.name}</h1> Here is your QR Code <img src='cid:qrcode.png'>")
    |> text_body("Hello #{user.name}. Please find your QR Code attached.\n")
    |> attachment(
      Swoosh.Attachment.new(
        {:data, Avengers.Accounts.OTP.qrcode_png(user)},                                                                                     
        filename: "qrcode.png",
        content_type: "image/png",
        type: :inline)
    )
    |> Avengers.Mailer.deliver

Maybe OTP (One Time Password) isn't the best in that example. Using "OTP" for anything but Open Telecom Platform in Elixir might be against some peoples religion. But TwoFA.qrcode_png(user) is ugly. TwoFactorAuth.qrcode_png(user) or TFA.qrcode_png(user)? In any case seeing the <img src='cid:qrcode.png'> is where my oh yeah, I'm stupid kicked in.

BTW, Great job on a mail system!

@princemaple
Copy link
Member

Thanks for that. I'll update the docs to include your example.

Documentation PRs are always welcome.

@princemaple
Copy link
Member

Pushed more docs updates.

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

Successfully merging this pull request may close these issues.

4 participants