PageRenderTime 55ms CodeModel.GetById 11ms app.highlight 40ms RepoModel.GetById 1ms app.codeStats 0ms

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