/src/smtp/z_email_receive.erl

https://code.google.com/p/zotonic/ · Erlang · 239 lines · 185 code · 29 blank · 25 comment · 6 complexity · ccd6d3ff4e8cfc5036670e0ebf739905 MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @copyright 2011 Marc Worrell
  3. %% @doc Handle received e-mail.
  4. %% Copyright 2011 Marc Worrell
  5. %%
  6. %% Licensed under the Apache License, Version 2.0 (the "License");
  7. %% you may not use this file except in compliance with the License.
  8. %% You may obtain a copy of the License at
  9. %%
  10. %% http://www.apache.org/licenses/LICENSE-2.0
  11. %%
  12. %% Unless required by applicable law or agreed to in writing, software
  13. %% distributed under the License is distributed on an "AS IS" BASIS,
  14. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. %% See the License for the specific language governing permissions and
  16. %% limitations under the License.
  17. -module(z_email_receive).
  18. -author("Marc Worrell <marc@worrell.nl>").
  19. -export([
  20. received/9,
  21. get_host/1
  22. ]).
  23. -include_lib("zotonic.hrl").
  24. -include_lib("zotonic_log.hrl").
  25. %% @doc Handle a received e-mail
  26. received(Recipients, From, Peer, Reference, {Type, Subtype}, Headers, Params, Body, Data) ->
  27. ParsedEmail = parse_email({Type, Subtype}, Headers, Params, Body),
  28. ParsedEmail1 = generate_text(generate_html(ParsedEmail)),
  29. ParsedEmail2 = ParsedEmail1#email{
  30. subject=proplists:get_value(<<"Subject">>, Headers),
  31. to=Recipients,
  32. from=From
  33. },
  34. [
  35. case get_host(Recipient) of
  36. {ok, LocalPart, LocalTags, Domain, Host} ->
  37. Context = z_context:new(Host),
  38. z_notifier:notify({log, #log_email{
  39. severity = ?LOG_INFO,
  40. mailer_status = received,
  41. mailer_host = z_convert:ip_to_list(Peer),
  42. message_nr = Reference,
  43. envelop_to = Recipient,
  44. envelop_from = From,
  45. props = [
  46. {headers, Headers}
  47. ]
  48. }},
  49. Context),
  50. z_notifier:notify(#email_received{
  51. localpart=LocalPart,
  52. localtags=LocalTags,
  53. domain=Domain,
  54. to=Recipient,
  55. from=From,
  56. reference=Reference,
  57. email=ParsedEmail2,
  58. headers=Headers,
  59. decoded={Type, Subtype, Headers, Params, Body},
  60. raw=Data
  61. },
  62. Context);
  63. undefined ->
  64. error_logger:info_msg("SMTP Dropping message, unknown host for recipient: ~p", [Recipient]),
  65. skip
  66. end
  67. || Recipient <- Recipients
  68. ].
  69. get_host(Recipient) ->
  70. [Username, Domain] = binstr:split(Recipient, <<"@">>, 2),
  71. [LocalPart|LocalTags] = binstr:split(Username, <<"+">>),
  72. case z_sites_dispatcher:get_host_for_domain(Domain) of
  73. {ok, Host} ->
  74. {ok, LocalPart, LocalTags, Domain, Host};
  75. undefined ->
  76. undefined
  77. end.
  78. %% @doc Parse an #email_received to a sanitized #email try to make sense of all parts
  79. %% All bodies are converted from the charset in the headers to the UTF-8
  80. %% This might not be correct for HTML bodies, where we have to check the content-type in
  81. %% the header (but then not in all cases...)
  82. parse_email({<<"text">>, <<"plain">>}, _Headers, Params, Body) ->
  83. case opt_attachment({<<"text">>, <<"plain">>}, Params, Body) of
  84. #email{} = E -> E;
  85. undefined -> #email{text=Body}
  86. end;
  87. parse_email({<<"text">>, <<"html">>}, _Headers, Params, Body) ->
  88. case opt_attachment({<<"text">>, <<"html">>}, Params, Body) of
  89. #email{} = E -> E;
  90. undefined -> #email{html=z_html:sanitize(Body)}
  91. end;
  92. parse_email({<<"multipart">>, <<"alternative">>}, Headers, _Params, Body) ->
  93. Parts = [
  94. parse_email({PartType, PartSubType}, PartHs, PartPs, PartBody)
  95. || {PartType, PartSubType, PartHs, PartPs, PartBody} <- Body
  96. ],
  97. lists:foldr(fun(A,B) -> merge_email(A,B) end,
  98. #email{subject=proplists:get_value(<<"Subject">>, Headers)},
  99. Parts);
  100. parse_email({<<"multipart">>, <<"related">>}, Headers, _Params, Body) ->
  101. Parts = [
  102. parse_email({PartType, PartSubType}, PartHs, PartPs, PartBody)
  103. || {PartType, PartSubType, PartHs, PartPs, PartBody} <- Body
  104. ],
  105. lists:foldr(fun(A,B) -> merge_email(A,B) end,
  106. #email{subject=proplists:get_value(<<"Subject">>, Headers)},
  107. Parts);
  108. parse_email({<<"multipart">>, <<"mixed">>}, Headers, _Params, Body) ->
  109. Parts = [
  110. parse_email({PartType, PartSubType}, PartHs, PartPs, PartBody)
  111. || {PartType, PartSubType, PartHs, PartPs, PartBody} <- Body
  112. ],
  113. lists:foldr(fun(A,B) -> append_email(A,B) end,
  114. #email{subject=proplists:get_value(<<"Subject">>, Headers)},
  115. Parts);
  116. parse_email({<<"multipart">>, <<"digest">>}, Headers, _Params, Body) ->
  117. Parts = [
  118. parse_email({PartType, PartSubType}, PartHs, PartPs, PartBody)
  119. || {PartType, PartSubType, PartHs, PartPs, PartBody} <- Body
  120. ],
  121. lists:foldr(fun(A,B) -> append_email(A,B) end,
  122. #email{subject=proplists:get_value(<<"Subject">>, Headers)},
  123. Parts);
  124. parse_email({<<"message">>, <<"rfc822">>}, _Headers, _Params, Body) ->
  125. {FwdType, FwdSubType, FwdHeaders, FwdParameters, FwdBody} = Body,
  126. Cc = proplists:get_value(<<"Cc">>, FwdHeaders),
  127. Text = iolist_to_binary([
  128. <<"From: ">>, proplists:get_value(<<"From">>, FwdHeaders, <<>>), $\n,
  129. <<"To: ">>, proplists:get_value(<<"To">>, FwdHeaders, <<>>), $\n,
  130. case Cc of
  131. undefined -> <<>>;
  132. _ -> [<<"Cc: ">>, Cc, $\n]
  133. end,
  134. <<"Subject: ">>, proplists:get_value(<<"Subject">>, FwdHeaders, <<>>), $\n,
  135. <<"Date: ">>, proplists:get_value(<<"Date">>, FwdHeaders, <<>>), $\n,
  136. $\n
  137. ]),
  138. Html = iolist_to_binary([
  139. <<"<dl class='rfc822-header'>">>,
  140. <<"<dt>From:</dt><dd>">>, z_html:escape(proplists:get_value(<<"From">>, FwdHeaders, <<>>)), <<"</dd>">>,
  141. <<"<dt>To:</dt><dd>">>, z_html:escape(proplists:get_value(<<"To">>, FwdHeaders, <<>>)), <<"</dd>">>,
  142. case Cc of
  143. undefined -> <<>>;
  144. _ -> [<<"<dt>Cc:</dt><dd>">>, z_html:escape(Cc), <<"</dd>">>]
  145. end,
  146. <<"<dt>Subject:</dt><dd>">>, z_html:escape(proplists:get_value(<<"Subject">>, FwdHeaders, <<>>)), <<"</dd>">>,
  147. <<"<dt>Date:</dt><dd>">>, z_html:escape(proplists:get_value(<<"Date">>, FwdHeaders, <<>>)), <<"</dd>">>,
  148. <<"</dl>">>
  149. ]),
  150. H = #email{html=Html, text=Text},
  151. E = parse_email({FwdType, FwdSubType}, FwdHeaders, FwdParameters, FwdBody),
  152. append_email(H, E);
  153. % Fallback to attachments
  154. parse_email(Mime, _Headers, Params, Body) ->
  155. attachment(Mime, Params, Body).
  156. % Merge two email parts
  157. merge_email(A, B) ->
  158. A#email{
  159. subject=take_defined(A#email.subject, B#email.subject),
  160. text=take_defined(A#email.text, B#email.text),
  161. html=take_defined(A#email.html, B#email.html),
  162. attachments=A#email.attachments++B#email.attachments
  163. }.
  164. % Append two e-mails (used in multipart/mixed messages)
  165. append_email(A, B) ->
  166. A1 = generate_text(generate_html(A)),
  167. B1 = generate_text(generate_html(B)),
  168. #email{
  169. subject=take_defined(A1#email.subject, B1#email.subject),
  170. html=append(A1#email.html, B1#email.html),
  171. text=append(A1#email.text, B1#email.text),
  172. attachments=A1#email.attachments++B1#email.attachments
  173. }.
  174. take_defined(undefined, B) -> B;
  175. take_defined(A, _) -> A.
  176. append(undefined, B) -> B;
  177. append(A, undefined) -> A;
  178. append(A, B) -> <<A/binary, B/binary>>.
  179. generate_text(#email{text=undefined, html=undefined} = E) ->
  180. E;
  181. generate_text(#email{text=undefined, html=Html} = E) ->
  182. E#email{text=z_markdown:to_markdown(Html, [no_html])};
  183. generate_text(E) ->
  184. E.
  185. generate_html(#email{text=undefined, html=undefined} = E) ->
  186. E;
  187. generate_html(#email{text=Text, html=undefined} = E) ->
  188. E#email{html=z_html:escape_link(Text)};
  189. generate_html(E) ->
  190. E.
  191. %% @doc Return a #email with an attachment if the disposition is attachment
  192. opt_attachment(Mime, Params, Body) ->
  193. case proplists:get_value(<<"disposition">>, Params, <<"inline">>) of
  194. <<"attachment">> -> attachment(Mime, Params, Body);
  195. _ -> undefined
  196. end.
  197. attachment({Type, SubType}, Params, Body) ->
  198. DispParams = proplists:get_value(<<"disposition-params">>, Params, []),
  199. #email{
  200. attachments=[
  201. #upload{
  202. mime=append_mime(Type, SubType),
  203. data=Body,
  204. filename=proplists:get_value(<<"filename">>, DispParams)
  205. }
  206. ]
  207. }.
  208. append_mime(undefined, B) -> B;
  209. append_mime(A, undefined) -> A;
  210. append_mime(A, B) -> <<A/binary, $/, B/binary>>.