/src/smtp/z_email_server.erl

http://github.com/zotonic/zotonic · Erlang · 1126 lines · 881 code · 114 blank · 131 comment · 14 complexity · 1039a498f18513ffde76f2a5c2bef079 MD5 · raw file

  1. %% @author Marc Worrell <marc@worrell.nl>
  2. %% @author Atilla Erdodi <atilla@maximonster.com>
  3. %% @copyright 2010-2015 Maximonster Interactive Things
  4. %% @doc Email server. Queues, renders and sends e-mails.
  5. %% Copyright 2010-2015 Maximonster Interactive Things
  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. -module(z_email_server).
  19. -author("Atilla Erdodi <atilla@maximonster.com>").
  20. -author("Marc Worrell <marc@worrell.nl>").
  21. -behaviour(gen_server).
  22. %% gen_server exports
  23. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
  24. %% interface functions
  25. -export([
  26. start_link/0,
  27. is_bounce_email_address/1,
  28. bounced/2,
  29. generate_message_id/0,
  30. send/2,
  31. send/3,
  32. tempfile/0,
  33. is_tempfile/1,
  34. is_tempfile_deletable/1,
  35. is_sender_enabled/2,
  36. is_sender_enabled/3
  37. ]).
  38. -include_lib("zotonic.hrl").
  39. -include_lib("stdlib/include/qlc.hrl").
  40. % Maximum times we retry to send a message before we mark it as failed.
  41. -define(MAX_RETRY, 10).
  42. % Max number of e-mails being sent at the same time
  43. -define(EMAIL_MAX_SENDING, 100).
  44. % Max number of connections per (relay) domain.
  45. -define(EMAIL_MAX_DOMAIN, 5).
  46. % Extension of files with queued copies of tmpfile attachments
  47. -define(TMPFILE_EXT, ".mailspool").
  48. -record(state, {smtp_relay, smtp_relay_opts, smtp_no_mx_lookups,
  49. smtp_verp_as_from, smtp_bcc, override,
  50. sending=[], delete_sent_after}).
  51. -record(email_queue, {id, retry_on=inc_timestamp(os:timestamp(), 1), retry=0,
  52. recipient, email, created=os:timestamp(), sent,
  53. pickled_context}).
  54. -record(email_sender, {id, sender_pid, domain, is_connected=false}).
  55. %%====================================================================
  56. %% API
  57. %%====================================================================
  58. %% @spec start_link() -> {ok,Pid} | ignore | {error,Error}
  59. %% @doc Starts the server
  60. start_link() ->
  61. start_link([]).
  62. %% @spec start_link(Args::list()) -> {ok,Pid} | ignore | {error,Error}
  63. %% @doc Starts the server
  64. start_link(Args) when is_list(Args) ->
  65. gen_server:start_link({local, ?MODULE}, ?MODULE, Args, []).
  66. %% @doc Check if the received e-mail address is a bounce address
  67. is_bounce_email_address(<<"noreply+",_/binary>>) -> true;
  68. is_bounce_email_address("noreply+"++_) -> true;
  69. is_bounce_email_address(_) -> false.
  70. %% @doc Handle a bounce
  71. bounced(Peer, NoReplyEmail) ->
  72. gen_server:cast(?MODULE, {bounced, Peer, NoReplyEmail}).
  73. %% @doc Generate a new message id
  74. generate_message_id() ->
  75. z_ids:random_id('az09', 20).
  76. %% @doc Send an email
  77. send(#email{} = Email, Context) ->
  78. send(generate_message_id(), Email, Context).
  79. %% @doc Send an email using a predefined unique id.
  80. send(Id, #email{} = Email, Context) ->
  81. case is_sender_enabled(Email, Context) of
  82. true ->
  83. Email1 = copy_attachments(Email),
  84. Context1 = z_context:depickle(z_context:pickle(Context)),
  85. gen_server:cast(?MODULE, {send, Id, Email1, Context1}),
  86. {ok, Id};
  87. false ->
  88. {error, sender_disabled}
  89. end.
  90. %% @doc Return the filename for a tempfile that can be used for the emailer
  91. tempfile() ->
  92. z_tempfile:tempfile(?TMPFILE_EXT).
  93. %% @doc Check if a file is a tempfile of the emailer
  94. is_tempfile(File) when is_list(File) ->
  95. z_tempfile:is_tempfile(File) andalso filename:extension(File) =:= ?TMPFILE_EXT.
  96. %% @doc Return the max age of a tempfile
  97. is_tempfile_deletable(undefined) ->
  98. false;
  99. is_tempfile_deletable(File) ->
  100. case is_tempfile(File) of
  101. true ->
  102. case filelib:last_modified(File) of
  103. 0 ->
  104. false;
  105. Modified when is_tuple(Modified) ->
  106. ModifiedSecs = calendar:datetime_to_gregorian_seconds(Modified),
  107. NowSecs = calendar:datetime_to_gregorian_seconds(calendar:local_time()),
  108. NowSecs > max_tempfile_age() + ModifiedSecs
  109. end;
  110. false ->
  111. true
  112. end.
  113. %% @doc Max tempfile age in seconds
  114. max_tempfile_age() ->
  115. max_tempfile_age(?MAX_RETRY, 0) + 24*3600.
  116. max_tempfile_age(0, Acc) -> Acc;
  117. max_tempfile_age(N, Acc) -> max_tempfile_age(N-1, period(N) + Acc).
  118. %% @doc Check if the sender is allowed to send email. If an user is disabled they are only
  119. %% allowed to send mail to themselves or to the admin.
  120. is_sender_enabled(#email{} = Email, Context) ->
  121. is_sender_enabled(z_acl:user(Context), Email#email.to, Context).
  122. is_sender_enabled(undefined, _RecipientEmail, _Context) ->
  123. true;
  124. is_sender_enabled(1, _RecipientEmail, _Context) ->
  125. true;
  126. is_sender_enabled(Id, RecipientEmail, Context) when is_list(RecipientEmail) ->
  127. is_sender_enabled(Id, z_convert:to_binary(RecipientEmail), Context);
  128. is_sender_enabled(Id, RecipientEmail, Context) when is_integer(Id) ->
  129. (m_rsc:exists(Id, Context) andalso z_convert:to_bool(m_rsc:p_no_acl(Id, is_published, Context)))
  130. orelse recipient_is_user_or_admin(Id, RecipientEmail, Context).
  131. recipient_is_user_or_admin(Id, RecipientEmail, Context) ->
  132. m_config:get_value(zotonic, admin_email, Context) =:= RecipientEmail
  133. orelse m_rsc:p_no_acl(1, email, Context) =:= RecipientEmail
  134. orelse m_rsc:p_no_acl(Id, email, Context) =:= RecipientEmail
  135. orelse lists:any(fun(Idn) ->
  136. proplists:get_value(key, Idn) =:= RecipientEmail
  137. end,
  138. m_identity:get_rsc_by_type(Id, email, Context)).
  139. %%====================================================================
  140. %% gen_server callbacks
  141. %%====================================================================
  142. %% @spec init(Args) -> {ok, State} |
  143. %% {ok, State, Timeout} |
  144. %% ignore |
  145. %% {stop, Reason}
  146. %% @doc Initiates the server.
  147. init(_Args) ->
  148. ok = create_email_queue(),
  149. timer:send_interval(5000, poll),
  150. State = update_config(#state{}),
  151. process_flag(trap_exit, true),
  152. {ok, State}.
  153. %% @spec handle_call(Request, From, State) -> {reply, Reply, State} |
  154. %% {reply, Reply, State, Timeout} |
  155. %% {noreply, State} |
  156. %% {noreply, State, Timeout} |
  157. %% {stop, Reason, Reply, State} |
  158. %% {stop, Reason, State}
  159. handle_call({is_sending_allowed, Pid, Relay}, _From, State) ->
  160. DomainWorkers = length(lists:filter(
  161. fun(#email_sender{domain=Domain, is_connected=IsConnected}) ->
  162. IsConnected andalso Relay =:= Domain
  163. end,
  164. State#state.sending)),
  165. case DomainWorkers < ?EMAIL_MAX_DOMAIN of
  166. true ->
  167. Workers = [
  168. case E#email_sender.sender_pid of
  169. Pid -> E#email_sender{is_connected=true};
  170. _ -> E
  171. end
  172. || E <- State#state.sending
  173. ],
  174. {reply, ok, State#state{sending=Workers}};
  175. false ->
  176. {reply, {error, wait}, State}
  177. end;
  178. %% @doc Trap unknown calls
  179. handle_call(Message, _From, State) ->
  180. {stop, {unknown_call, Message}, State}.
  181. %% @spec handle_cast(Msg, State) -> {noreply, State} |
  182. %% {noreply, State, Timeout} |
  183. %% {stop, Reason, State}
  184. %% @doc Send an e-mail.
  185. handle_cast({send, Id, #email{} = Email, Context}, State) ->
  186. State1 = update_config(State),
  187. State2 = case z_utils:is_empty(Email#email.to) of
  188. true -> State1;
  189. false -> send_email(Id, Email#email.to, Email, Context, State1)
  190. end,
  191. State3 = case z_utils:is_empty(Email#email.cc) of
  192. true -> State2;
  193. false -> send_email(<<Id/binary, "+cc">>, Email#email.cc, Email, Context, State2)
  194. end,
  195. State4 = case z_utils:is_empty(Email#email.bcc) of
  196. true -> State3;
  197. false -> send_email(<<Id/binary, "+bcc">>, Email#email.bcc, Email, Context, State3)
  198. end,
  199. {noreply, State4};
  200. %%@ doc Handle a bounced email
  201. handle_cast({bounced, Peer, BounceEmail}, State) ->
  202. % Fetch the MsgId from the bounce address
  203. [BounceLocalName,Domain] = binstr:split(z_convert:to_binary(BounceEmail), <<"@">>),
  204. <<"noreply+", MsgId/binary>> = BounceLocalName,
  205. % Find the original message in our database of recent sent e-mail
  206. TrFun = fun()->
  207. [QEmail] = mnesia:read(email_queue, MsgId),
  208. mnesia:delete_object(QEmail),
  209. {(QEmail#email_queue.email)#email.to, QEmail#email_queue.pickled_context}
  210. end,
  211. case mnesia:transaction(TrFun) of
  212. {atomic, {Recipient, PickledContext}} ->
  213. Context = z_context:depickle(PickledContext),
  214. z_notifier:notify(#email_bounced{
  215. message_nr=MsgId,
  216. recipient=Recipient
  217. }, Context),
  218. z_notifier:notify(#zlog{
  219. user_id=z_acl:user(Context),
  220. props=#log_email{
  221. severity = ?LOG_ERROR,
  222. message_nr = MsgId,
  223. mailer_status = bounce,
  224. mailer_host = z_convert:ip_to_list(Peer),
  225. envelop_to = BounceEmail,
  226. envelop_from = "<>",
  227. to_id = z_acl:user(Context),
  228. props = []
  229. }}, Context);
  230. _ ->
  231. % We got a bounce, but we don't have the message anymore.
  232. % Custom bounce domains make this difficult to process.
  233. case z_sites_dispatcher:get_host_for_domain(Domain) of
  234. {ok, Host} ->
  235. Context = z_context:new(Host),
  236. z_notifier:notify(#email_bounced{
  237. message_nr=MsgId,
  238. recipient=undefined
  239. }, Context),
  240. z_notifier:notify(#zlog{
  241. user_id=undefined,
  242. props=#log_email{
  243. severity = ?LOG_WARNING,
  244. message_nr = MsgId,
  245. mailer_status = bounce,
  246. mailer_host = z_convert:ip_to_list(Peer),
  247. envelop_to = BounceEmail,
  248. envelop_from = "<>",
  249. props = []
  250. }}, Context);
  251. undefined ->
  252. ignore
  253. end
  254. end,
  255. {noreply, State};
  256. %% @doc Trap unknown casts
  257. handle_cast(Message, State) ->
  258. {stop, {unknown_cast, Message}, State}.
  259. %% @spec handle_info(Info, State) -> {noreply, State} |
  260. %% {noreply, State, Timeout} |
  261. %% {stop, Reason, State}
  262. %% @doc Poll the database queue for any retrys.
  263. handle_info(poll, State) ->
  264. State1 = poll_queued(State),
  265. z_utils:flush_message(poll),
  266. {noreply, State1};
  267. %% @doc Spawned process has crashed. Clear it from the sending list.
  268. handle_info({'EXIT', Pid, _Reason}, State) ->
  269. {noreply, remove_worker(Pid, State)};
  270. %% @doc Handling all non call/cast messages
  271. handle_info(_Info, State) ->
  272. {noreply, State}.
  273. %% @spec terminate(Reason, State) -> void()
  274. %% @doc This function is called by a gen_server when it is about to
  275. %% terminate. It should be the opposite of Module:init/1 and do any necessary
  276. %% cleaning up. When it returns, the gen_server terminates with Reason.
  277. %% The return value is ignored.
  278. terminate(_Reason, _State) ->
  279. ok.
  280. %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
  281. %% @doc Convert process state when code is changed
  282. code_change(_OldVsn, State, _Extra) ->
  283. {ok, State}.
  284. %%====================================================================
  285. %% support functions
  286. %%====================================================================
  287. %% @doc Create the email queue in mnesia
  288. create_email_queue() ->
  289. TabDef = [
  290. {type, set},
  291. {record_name, email_queue},
  292. {attributes, record_info(fields, email_queue)}
  293. | case application:get_env(mnesia, dir) of
  294. {ok, _} -> [ {disc_copies, [node()]} ];
  295. undefined -> []
  296. end
  297. ],
  298. case mnesia:create_table(email_queue, TabDef) of
  299. {atomic, ok} -> ok;
  300. {aborted, {already_exists, email_queue}} -> ok
  301. end.
  302. %% @doc Refetch the emailer configuration so that we adapt to any config changes.
  303. update_config(State) ->
  304. SmtpRelay = z_config:get(smtp_relay),
  305. SmtpRelayOpts =
  306. case SmtpRelay of
  307. true ->
  308. [{relay, z_config:get(smtp_host, "localhost")},
  309. {port, z_config:get(smtp_port, 25)},
  310. {ssl, z_config:get(smtp_ssl, false)}]
  311. ++ case {z_config:get(smtp_username),
  312. z_config:get(smtp_password)} of
  313. {undefined, undefined} ->
  314. [];
  315. {User, Pass} ->
  316. [{auth, always},
  317. {username, User},
  318. {password, Pass}]
  319. end;
  320. false ->
  321. []
  322. end,
  323. SmtpNoMxLookups = z_config:get(smtp_no_mx_lookups),
  324. SmtpVerpAsFrom = z_config:get(smtp_verp_as_from),
  325. SmtpBcc = z_config:get(smtp_bcc),
  326. Override = z_config:get(email_override),
  327. DeleteSentAfter = z_config:get(smtp_delete_sent_after),
  328. State#state{smtp_relay=SmtpRelay,
  329. smtp_relay_opts=SmtpRelayOpts,
  330. smtp_no_mx_lookups=SmtpNoMxLookups,
  331. smtp_verp_as_from=SmtpVerpAsFrom,
  332. smtp_bcc=SmtpBcc,
  333. override=Override,
  334. delete_sent_after=DeleteSentAfter}.
  335. %% @doc Get the bounce email address. Can be overridden per site in config setting site.bounce_email_override.
  336. bounce_email(MessageId, Context) ->
  337. case m_config:get_value(site, bounce_email_override, Context) of
  338. undefined ->
  339. case z_config:get(smtp_bounce_email_override) of
  340. undefined -> "noreply+"++MessageId;
  341. VERP -> z_convert:to_list(VERP)
  342. end;
  343. VERP ->
  344. z_convert:to_list(VERP)
  345. end.
  346. reply_email(MessageId, Context) ->
  347. "reply+"++z_convert:to_list(MessageId)++[$@ | z_email:email_domain(Context)].
  348. % The 'From' is either the message id (and bounce domain) or the set from.
  349. get_email_from(EmailFrom, VERP, State, Context) ->
  350. From = case EmailFrom of
  351. L when L =:= [] orelse L =:= undefined orelse L =:= <<>> ->
  352. get_email_from(Context);
  353. _ -> EmailFrom
  354. end,
  355. case State#state.smtp_verp_as_from of
  356. true ->
  357. {FromName, _FromEmail} = z_email:split_name_email(From),
  358. string:strip(FromName ++ " " ++ VERP);
  359. _ ->
  360. {FromName, FromEmail} = z_email:split_name_email(From),
  361. case FromEmail of
  362. [] -> string:strip(FromName ++ " <" ++ get_email_from(Context) ++ ">");
  363. _ -> From
  364. end
  365. end.
  366. % When the 'From' is not the VERP then the 'From' is derived from the site
  367. get_email_from(Context) ->
  368. %% Let the default be overruled by the config setting
  369. case m_config:get_value(site, email_from, Context) of
  370. undefined -> "noreply@" ++ z_email:email_domain(Context);
  371. EmailFrom -> z_convert:to_list(EmailFrom)
  372. end.
  373. % Unique message-id, depends on bounce domain
  374. message_id(MessageId, Context) ->
  375. z_convert:to_list(MessageId)++[$@ | z_email:bounce_domain(Context)].
  376. %% @doc Remove a worker Pid from the server state.
  377. remove_worker(Pid, State) ->
  378. Filtered = lists:filter(fun(#email_sender{sender_pid=P}) -> P =/= Pid end, State#state.sending),
  379. State#state{sending=Filtered}.
  380. %% =========================
  381. %% SENDING related functions
  382. %% =========================
  383. % Send an email
  384. send_email(Id, Recipient, Email, Context, State) ->
  385. QEmail = #email_queue{id=Id,
  386. recipient=Recipient,
  387. email=Email,
  388. retry_on=inc_timestamp(os:timestamp(), 0),
  389. sent=undefined,
  390. pickled_context=z_context:pickle(Context)},
  391. QEmailTransFun = fun() -> mnesia:write(QEmail) end,
  392. {atomic, ok} = mnesia:transaction(QEmailTransFun),
  393. case Email#email.queue orelse length(State#state.sending) > ?EMAIL_MAX_SENDING of
  394. true -> State;
  395. false -> spawn_send(Id, Recipient, Email, Context, State)
  396. end.
  397. spawn_send(Id, Recipient, Email, Context, State) ->
  398. case lists:keyfind(Id, #email_sender.id, State#state.sending) =/= false of
  399. false ->
  400. spawn_send_check_email(Id, Recipient, Email, Context, State);
  401. _ ->
  402. %% Is already being sent. Do nothing, it will retry later
  403. State
  404. end.
  405. spawn_send_check_email(Id, Recipient, Email, Context, State) ->
  406. case is_sender_enabled(Email, Context) of
  407. true ->
  408. case is_valid_email(Recipient) of
  409. true ->
  410. spawn_send_checked(Id, Recipient, Email, Context, State);
  411. false ->
  412. %% delete email from the queue and notify the system
  413. delete_email(illegal_address, Id, Recipient, Email, Context),
  414. State
  415. end;
  416. false ->
  417. delete_email(sender_disabled, Id, Recipient, Email, Context),
  418. State
  419. end.
  420. delete_email(Error, Id, Recipient, Email, Context) ->
  421. delete_emailq(Id),
  422. z_notifier:first(#email_failed{
  423. message_nr=Id,
  424. recipient=Recipient,
  425. is_final=true,
  426. status= case Error of
  427. illegal_address -> <<"Malformed email address">>;
  428. sender_disabled -> <<"Sender disabled">>
  429. end,
  430. reason=Error
  431. }, Context),
  432. LogEmail = #log_email{
  433. severity=?LOG_ERROR,
  434. mailer_status=error,
  435. props=[{reason, Error}],
  436. message_nr=Id,
  437. envelop_to=Recipient,
  438. envelop_from="",
  439. to_id=proplists:get_value(recipient_id, Email#email.vars),
  440. from_id=z_acl:user(Context),
  441. content_id=proplists:get_value(id, Email#email.vars),
  442. other_id=proplists:get_value(list_id, Email#email.vars),
  443. message_template=Email#email.html_tpl
  444. },
  445. z_notifier:notify(#zlog{user_id=z_acl:user(Context), props=LogEmail}, Context).
  446. % Start a worker, prevent too many workers per domain.
  447. spawn_send_checked(Id, Recipient, Email, Context, State) ->
  448. Recipient1 = check_override(Recipient, m_config:get_value(site, email_override, Context), State),
  449. Recipient2 = string:strip(z_string:line(binary_to_list(z_convert:to_binary(Recipient1)))),
  450. {_RcptName, RecipientEmail} = z_email:split_name_email(Recipient2),
  451. [_RcptLocalName, RecipientDomain] = string:tokens(RecipientEmail, "@"),
  452. SmtpOpts = [
  453. {no_mx_lookups, State#state.smtp_no_mx_lookups},
  454. {hostname, z_email:email_domain(Context)}
  455. | case State#state.smtp_relay of
  456. true -> State#state.smtp_relay_opts;
  457. false -> [{relay, RecipientDomain}]
  458. end
  459. ],
  460. BccSmtpOpts = case z_utils:is_empty(State#state.smtp_bcc) of
  461. true ->
  462. [];
  463. false ->
  464. {_BccName, BccEmail} = z_email:split_name_email(State#state.smtp_bcc),
  465. [_BccLocalName, BccDomain] = string:tokens(BccEmail, "@"),
  466. [
  467. {no_mx_lookups, State#state.smtp_no_mx_lookups},
  468. {hostname, z_email:email_domain(Context)}
  469. | case State#state.smtp_relay of
  470. true -> State#state.smtp_relay_opts;
  471. false -> [{relay, BccDomain}]
  472. end
  473. ]
  474. end,
  475. MessageId = message_id(Id, Context),
  476. VERP = "<"++bounce_email(MessageId, Context)++">",
  477. From = get_email_from(Email#email.from, VERP, State, Context),
  478. SenderPid = erlang:spawn_link(
  479. fun() ->
  480. spawned_email_sender(
  481. Id, MessageId, Recipient, RecipientEmail, VERP,
  482. From, State#state.smtp_bcc, Email, SmtpOpts, BccSmtpOpts,
  483. Context)
  484. end),
  485. {relay, Relay} = proplists:lookup(relay, SmtpOpts),
  486. State#state{
  487. sending=[
  488. #email_sender{id=Id, sender_pid=SenderPid, domain=Relay} | State#state.sending
  489. ]}.
  490. spawned_email_sender(Id, MessageId, Recipient, RecipientEmail, VERP, From,
  491. Bcc, Email, SmtpOpts, BccSmtpOpts, Context) ->
  492. EncodedMail = encode_email(Id, Email, "<"++MessageId++">", From, Context),
  493. spawned_email_sender_loop(Id, MessageId, Recipient, RecipientEmail, VERP, From,
  494. Bcc, Email, EncodedMail, SmtpOpts, BccSmtpOpts, Context).
  495. spawned_email_sender_loop(Id, MessageId, Recipient, RecipientEmail, VERP, From,
  496. Bcc, Email, EncodedMail, SmtpOpts, BccSmtpOpts, Context) ->
  497. {relay, Relay} = proplists:lookup(relay, SmtpOpts),
  498. case gen_server:call(?MODULE, {is_sending_allowed, self(), Relay}) of
  499. {error, wait} ->
  500. lager:debug("[smtp] Delaying email to ~p (~p), too many parallel senders for relay ~p",
  501. [RecipientEmail, Id, Relay]),
  502. timer:sleep(1000),
  503. spawned_email_sender(Id, MessageId, Recipient, RecipientEmail, VERP, From,
  504. Bcc, Email, SmtpOpts, BccSmtpOpts, Context);
  505. ok ->
  506. LogEmail = #log_email{
  507. message_nr=Id,
  508. envelop_to=RecipientEmail,
  509. envelop_from=VERP,
  510. to_id=proplists:get_value(recipient_id, Email#email.vars),
  511. from_id=z_acl:user(Context),
  512. content_id=proplists:get_value(id, Email#email.vars),
  513. other_id=proplists:get_value(list_id, Email#email.vars), %% Supposed to contain the mailinglist id
  514. message_template=Email#email.html_tpl
  515. },
  516. z_notifier:notify(#zlog{
  517. user_id=LogEmail#log_email.from_id,
  518. props=LogEmail#log_email{severity=?LOG_INFO, mailer_status=sending}
  519. }, Context),
  520. lager:info("[smtp] Sending email to ~p (~p), via relay ~p",
  521. [RecipientEmail, Id, Relay]),
  522. %% use the unique id as 'envelope sender' (VERP)
  523. case gen_smtp_client:send_blocking({VERP, [RecipientEmail], EncodedMail}, SmtpOpts) of
  524. {error, retries_exceeded, {_FailureType, Host, Message}} ->
  525. %% do nothing, it will retry later
  526. z_notifier:notify(#email_failed{
  527. message_nr=Id,
  528. recipient=Recipient,
  529. is_final=false,
  530. reason=retry,
  531. status=Message
  532. }, Context),
  533. z_notifier:notify(#zlog{
  534. user_id=LogEmail#log_email.from_id,
  535. props=LogEmail#log_email{
  536. severity=?LOG_WARNING,
  537. mailer_status=retry,
  538. mailer_message=Message,
  539. mailer_host=Host
  540. }
  541. }, Context),
  542. ok;
  543. {error, no_more_hosts, {permanent_failure, Host, Message}} ->
  544. % classify this as a permanent failure, something is wrong with the receiving server or the recipient
  545. z_notifier:notify(#email_failed{
  546. message_nr=Id,
  547. recipient=Recipient,
  548. is_final=true,
  549. reason=smtphost,
  550. status=Message
  551. }, Context),
  552. z_notifier:notify(#zlog{
  553. user_id=LogEmail#log_email.from_id,
  554. props=LogEmail#log_email{
  555. severity = ?LOG_ERROR,
  556. mailer_status = bounce,
  557. mailer_message = Message,
  558. mailer_host = Host
  559. }
  560. }, Context),
  561. % delete email from the queue and notify the system
  562. delete_emailq(Id);
  563. {error, Reason} ->
  564. % Returned when the options are not ok
  565. z_notifier:notify(#email_failed{
  566. message_nr=Id,
  567. recipient=Recipient,
  568. is_final=true,
  569. reason=error
  570. }, Context),
  571. z_notifier:notify(#zlog{
  572. user_id=LogEmail#log_email.from_id,
  573. props=LogEmail#log_email{
  574. severity=?LOG_ERROR,
  575. mailer_status=error,
  576. props=[{reason, Reason}]
  577. }
  578. }, Context),
  579. %% delete email from the queue and notify the system
  580. delete_emailq(Id);
  581. Receipt when is_binary(Receipt) ->
  582. z_notifier:notify(#email_sent{
  583. message_nr=Id,
  584. recipient=Recipient,
  585. is_final=false
  586. }, Context),
  587. z_notifier:notify(#zlog{
  588. user_id=LogEmail#log_email.from_id,
  589. props=LogEmail#log_email{
  590. severity=?LOG_INFO,
  591. mailer_status=sent,
  592. mailer_message=Receipt
  593. }
  594. }, Context),
  595. %% email accepted by relay
  596. mark_sent(Id),
  597. %% async send a copy for debugging if necessary
  598. case z_utils:is_empty(Bcc) of
  599. true ->
  600. ok;
  601. false ->
  602. catch gen_smtp_client:send({VERP, [Bcc], EncodedMail}, BccSmtpOpts)
  603. end
  604. end
  605. end.
  606. encode_email(_Id, #email{raw=Raw}, _MessageId, _From, _Context) when is_list(Raw); is_binary(Raw) ->
  607. z_convert:to_binary([
  608. "X-Mailer: Zotonic ", ?ZOTONIC_VERSION, " (http://zotonic.com)\r\n",
  609. Raw
  610. ]);
  611. encode_email(Id, #email{body=undefined} = Email, MessageId, From, Context) ->
  612. %% Optionally render the text and html body
  613. Vars = [{email_to, Email#email.to}, {email_from, From} | Email#email.vars],
  614. ContextRender = set_recipient_prefs(Vars, Context),
  615. Text = optional_render(Email#email.text, Email#email.text_tpl, Vars, ContextRender),
  616. Html = optional_render(Email#email.html, Email#email.html_tpl, Vars, ContextRender),
  617. %% Fetch the subject from the title of the HTML part or from the Email record
  618. Subject = case {Html, Email#email.subject} of
  619. {[], undefined} ->
  620. <<>>;
  621. {_Html, undefined} ->
  622. {match, [_, {Start,Len}|_]} = re:run(Html, "<title>(.*?)</title>", [dotall, caseless]),
  623. string:strip(z_string:line(z_html:unescape(lists:sublist(Html, Start+1, Len))));
  624. {_Html, Sub} ->
  625. Sub
  626. end,
  627. Headers = [{"From", From},
  628. {"To", z_convert:to_list(Email#email.to)},
  629. {"Subject", z_convert:to_flatlist(Subject)},
  630. {"Date", date(Context)},
  631. {"MIME-Version", "1.0"},
  632. {"Message-ID", MessageId},
  633. {"X-Mailer", "Zotonic " ++ ?ZOTONIC_VERSION ++ " (http://zotonic.com)"}],
  634. Headers2 = add_reply_to(Id, Email, add_cc(Email, Headers), Context),
  635. build_and_encode_mail(Headers2, Text, Html, Email#email.attachments, Context);
  636. encode_email(Id, #email{body=Body} = Email, MessageId, From, Context) when is_tuple(Body) ->
  637. Headers = [{<<"From">>, From},
  638. {<<"To">>, Email#email.to},
  639. {<<"Message-ID">>, MessageId},
  640. {<<"X-Mailer">>, "Zotonic " ++ ?ZOTONIC_VERSION ++ " (http://zotonic.com)"}
  641. | Email#email.headers ],
  642. Headers2 = add_reply_to(Id, Email, add_cc(Email, Headers), Context),
  643. {BodyType, BodySubtype, BodyHeaders, BodyParams, BodyParts} = Body,
  644. MailHeaders = [
  645. {z_convert:to_binary(H), z_convert:to_binary(V)} || {H,V} <- (Headers2 ++ BodyHeaders)
  646. ],
  647. mimemail:encode({BodyType, BodySubtype, MailHeaders, BodyParams, BodyParts}, opt_dkim(Context));
  648. encode_email(Id, #email{body=Body} = Email, MessageId, From, Context) when is_list(Body); is_binary(Body) ->
  649. Headers = [{"From", From},
  650. {"To", z_convert:to_list(Email#email.to)},
  651. {"Message-ID", MessageId},
  652. {"X-Mailer", "Zotonic " ++ ?ZOTONIC_VERSION ++ " (http://zotonic.com)"}
  653. | Email#email.headers ],
  654. Headers2 = add_reply_to(Id, Email, add_cc(Email, Headers), Context),
  655. iolist_to_binary([ encode_headers(Headers2), "\r\n\r\n", Body ]).
  656. date(Context) ->
  657. z_convert:to_list(z_datetime:format("r", z_context:set_language(en, Context))).
  658. add_cc(#email{cc=undefined}, Headers) ->
  659. Headers;
  660. add_cc(#email{cc=[]}, Headers) ->
  661. Headers;
  662. add_cc(#email{cc=Cc}, Headers) ->
  663. Headers ++ [{"Cc", Cc}].
  664. add_reply_to(_Id, #email{reply_to=undefined}, Headers, _Context) ->
  665. Headers;
  666. add_reply_to(_Id, #email{reply_to = <<>>}, Headers, _Context) ->
  667. [{"Reply-To", "<>"} | Headers];
  668. add_reply_to(Id, #email{reply_to=message_id}, Headers, Context) ->
  669. [{"Reply-To", reply_email(Id, Context)} | Headers];
  670. add_reply_to(_Id, #email{reply_to=ReplyTo}, Headers, Context) ->
  671. {Name, Email} = z_email:split_name_email(ReplyTo),
  672. ReplyTo1 = string:strip(Name ++ " <" ++ z_email:ensure_domain(Email, Context) ++ ">"),
  673. [{"Reply-To", ReplyTo1} | Headers].
  674. build_and_encode_mail(Headers, Text, Html, Attachment, Context) ->
  675. Headers1 = [
  676. {z_convert:to_binary(H), z_convert:to_binary(V)} || {H,V} <- Headers
  677. ],
  678. Params = [
  679. {<<"content-type-params">>, [ {<<"charset">>, <<"utf-8">>} ]},
  680. {<<"disposition">>, <<"inline">>},
  681. {<<"transfer-encoding">>, <<"quoted-printable">>},
  682. {<<"disposition-params">>, []}
  683. ],
  684. Parts = case z_utils:is_empty(Text) of
  685. true ->
  686. case z_utils:is_empty(Html) of
  687. true ->
  688. [];
  689. false ->
  690. [{<<"text">>, <<"plain">>, [], Params,
  691. expand_cr(z_convert:to_binary(z_markdown:to_markdown(Html, [no_html])))}]
  692. end;
  693. false ->
  694. [{<<"text">>, <<"plain">>, [], Params,
  695. expand_cr(z_convert:to_binary(Text))}]
  696. end,
  697. Parts1 = case z_utils:is_empty(Html) of
  698. true ->
  699. Parts;
  700. false ->
  701. z_email_embed:embed_images(Parts ++ [{<<"text">>, <<"html">>, [], Params, z_convert:to_binary(Html)}], Context)
  702. end,
  703. case Attachment of
  704. [] ->
  705. case Parts1 of
  706. [{T,ST,[],Ps,SubParts}] -> mimemail:encode({T,ST,Headers1,Ps,SubParts}, opt_dkim(Context));
  707. _MultiPart -> mimemail:encode({<<"multipart">>, <<"alternative">>, Headers1, [], Parts1}, opt_dkim(Context))
  708. end;
  709. _ ->
  710. AttsEncoded = [ encode_attachment(Att, Context) || Att <- Attachment ],
  711. AttsEncodedOk = lists:filter(fun({error, _}) -> false; (_) -> true end, AttsEncoded),
  712. mimemail:encode({<<"multipart">>, <<"mixed">>,
  713. Headers1,
  714. [],
  715. [ {<<"multipart">>, <<"alternative">>, [], [], Parts1} | AttsEncodedOk ]
  716. }, opt_dkim(Context))
  717. end.
  718. encode_attachment(Att, Context) when is_integer(Att) ->
  719. case m_media:get(Att, Context) of
  720. undefined ->
  721. {error, no_medium};
  722. Props ->
  723. Upload = #upload{
  724. tmpfile=filename:join(z_path:media_archive(Context),
  725. proplists:get_value(filename, Props)),
  726. mime=proplists:get_value(mime, Props)
  727. },
  728. encode_attachment(Upload, Context)
  729. end;
  730. encode_attachment(#upload{mime=undefined, data=undefined, tmpfile=File, filename=Filename} = Att, Context) ->
  731. case z_media_identify:identify(File, Filename, Context) of
  732. {ok, Ps} ->
  733. Mime = proplists:get_value(mime, Ps, <<"application/octet-stream">>),
  734. encode_attachment(Att#upload{mime=Mime}, Context);
  735. {error, _} ->
  736. encode_attachment(Att#upload{mime= <<"application/octet-stream">>}, Context)
  737. end;
  738. encode_attachment(#upload{mime=undefined, filename=Filename} = Att, Context) ->
  739. Mime = z_media_identify:guess_mime(Filename),
  740. encode_attachment(Att#upload{mime=Mime}, Context);
  741. encode_attachment(#upload{} = Att, _Context) ->
  742. Data = case Att#upload.data of
  743. undefined ->
  744. {ok, FileData} = file:read_file(Att#upload.tmpfile),
  745. FileData;
  746. AttData ->
  747. AttData
  748. end,
  749. [Type, Subtype] = binstr:split(z_convert:to_binary(Att#upload.mime), <<"/">>, 2),
  750. {
  751. Type, Subtype,
  752. [],
  753. [
  754. {<<"transfer-encoding">>, <<"base64">>},
  755. {<<"disposition">>, <<"attachment">>},
  756. {<<"disposition-params">>, [{<<"filename">>, filename(Att)}]}
  757. ],
  758. Data
  759. }.
  760. filename(#upload{filename=undefined, tmpfile=undefined}) ->
  761. <<"untitled">>;
  762. filename(#upload{filename=undefined, tmpfile=Tmpfile}) ->
  763. z_convert:to_binary(filename:basename(z_convert:to_list(Tmpfile)));
  764. filename(#upload{filename=Filename}) ->
  765. z_convert:to_binary(Filename).
  766. % Make sure that loose \n characters are expanded to \r\n
  767. expand_cr(B) -> expand_cr(B, <<>>).
  768. expand_cr(<<>>, Acc) -> Acc;
  769. expand_cr(<<13, 10, R/binary>>, Acc) -> expand_cr(R, <<Acc/binary, 13, 10>>);
  770. expand_cr(<<10, R/binary>>, Acc) -> expand_cr(R, <<Acc/binary, 13, 10>>);
  771. expand_cr(<<13, R/binary>>, Acc) -> expand_cr(R, <<Acc/binary, 13, 10>>);
  772. expand_cr(<<C, R/binary>>, Acc) -> expand_cr(R, <<Acc/binary, C>>).
  773. check_override(EmailAddr, _SiteOverride, _State) when EmailAddr == undefined; EmailAddr == []; EmailAddr == <<>> ->
  774. undefined;
  775. check_override(EmailAddr, SiteOverride, #state{override=ZotonicOverride}) ->
  776. UseOverride = case z_utils:is_empty(ZotonicOverride) of
  777. true -> SiteOverride;
  778. false -> ZotonicOverride
  779. end,
  780. case z_utils:is_empty(UseOverride) of
  781. true ->
  782. z_convert:to_list(EmailAddr);
  783. false ->
  784. escape_email(z_convert:to_list(EmailAddr)) ++ " (override) <" ++ z_convert:to_list(UseOverride) ++ ">"
  785. end.
  786. escape_email(Email) ->
  787. escape_email(Email, []).
  788. escape_email([], Acc) ->
  789. lists:reverse(Acc);
  790. escape_email([$@|T], Acc) ->
  791. escape_email(T, [$-,$t,$a,$-|Acc]);
  792. escape_email([H|T], Acc) ->
  793. escape_email(T, [H|Acc]).
  794. optional_render(undefined, undefined, _Vars, _Context) ->
  795. [];
  796. optional_render(Text, undefined, _Vars, _Context) ->
  797. Text;
  798. optional_render(undefined, Template, Vars, Context) ->
  799. {Output, _Context} = z_template:render_to_iolist(Template, Vars, Context),
  800. binary_to_list(iolist_to_binary(Output)).
  801. set_recipient_prefs(Vars, Context) ->
  802. case proplists:get_value(recipient_id, Vars) of
  803. UserId when is_integer(UserId) ->
  804. z_notifier:foldl(#user_context{id=UserId}, Context, Context);
  805. _Other ->
  806. Context
  807. end.
  808. %% @doc Mark email as sent by adding the 'sent' timestamp.
  809. %% This will schedule it for deletion as well.
  810. mark_sent(Id) ->
  811. Tr = fun() ->
  812. case mnesia:read(email_queue, Id) of
  813. [QEmail] -> mnesia:write(QEmail#email_queue{sent=os:timestamp()});
  814. [] -> {error, notfound}
  815. end
  816. end,
  817. {atomic, Result} = mnesia:transaction(Tr),
  818. Result.
  819. %% @doc Deletes a message from the queue.
  820. delete_emailq(Id) ->
  821. Tr = fun()->
  822. [QEmail] = mnesia:read(email_queue, Id),
  823. mnesia:delete_object(QEmail)
  824. end,
  825. {atomic, ok} = mnesia:transaction(Tr).
  826. %%
  827. %% QUEUEING related functions
  828. %%
  829. %% @doc Fetch a new batch of queued e-mails. Deletes failed messages.
  830. poll_queued(State) ->
  831. %% delete sent messages
  832. Now = os:timestamp(),
  833. DelTransFun = fun() ->
  834. DelQuery = qlc:q([QEmail || QEmail <- mnesia:table(email_queue),
  835. QEmail#email_queue.sent =/= undefined andalso
  836. timer:now_diff(
  837. inc_timestamp(QEmail#email_queue.sent, State#state.delete_sent_after),
  838. Now) < 0
  839. ]),
  840. DelQueryRes = qlc:e(DelQuery),
  841. [ begin
  842. mnesia:delete_object(QEmail),
  843. {QEmail#email_queue.id,
  844. QEmail#email_queue.recipient,
  845. QEmail#email_queue.pickled_context}
  846. end || QEmail <- DelQueryRes ]
  847. end,
  848. {atomic, NotifyList1} = mnesia:transaction(DelTransFun),
  849. %% notify the system that these emails were sucessfully sent and (probably) received
  850. lists:foreach(fun({Id, Recipient, PickledContext}) ->
  851. z_notifier:notify(#email_sent{
  852. message_nr=Id,
  853. recipient=Recipient,
  854. is_final=true
  855. }, z_context:depickle(PickledContext))
  856. end,
  857. NotifyList1),
  858. %% delete all messages with too high retry count
  859. SetFailTransFun = fun() ->
  860. PollQuery = qlc:q([QEmail || QEmail <- mnesia:table(email_queue),
  861. QEmail#email_queue.sent =:= undefined,
  862. QEmail#email_queue.retry > ?MAX_RETRY]),
  863. PollQueryRes = qlc:e(PollQuery),
  864. [ begin
  865. mnesia:delete_object(QEmail),
  866. {QEmail#email_queue.id,
  867. QEmail#email_queue.recipient,
  868. QEmail#email_queue.pickled_context}
  869. end || QEmail <- PollQueryRes ]
  870. end,
  871. {atomic, NotifyList2} = mnesia:transaction(SetFailTransFun),
  872. %% notify the system that these emails were failed to be sent
  873. lists:foreach(fun({Id, Recipient, PickledContext}) ->
  874. z_notifier:first(#email_failed{
  875. message_nr=Id,
  876. recipient=Recipient,
  877. is_final=true,
  878. reason=retry,
  879. status= <<"Retries exceeded">>
  880. }, z_context:depickle(PickledContext))
  881. end,
  882. NotifyList2),
  883. MaxListSize = ?EMAIL_MAX_SENDING - length(State#state.sending),
  884. case MaxListSize > 0 of
  885. true ->
  886. %% fetch a batch of messages for sending
  887. FetchTransFun =
  888. fun() ->
  889. Q = qlc:q([QEmail || QEmail <- mnesia:table(email_queue),
  890. %% Should not already have been sent
  891. QEmail#email_queue.sent =:= undefined,
  892. %% Should not be currently sending
  893. proplists:get_value(QEmail#email_queue.id, State#state.sending) =:= undefined,
  894. %% Eligible for retry
  895. timer:now_diff(QEmail#email_queue.retry_on, Now) < 0]),
  896. QCursor = qlc:cursor(Q),
  897. QFound = qlc:next_answers(QCursor, MaxListSize),
  898. ok = qlc:delete_cursor(QCursor),
  899. QFound
  900. end,
  901. {atomic, Ms} = mnesia:transaction(FetchTransFun),
  902. %% send the fetched messages
  903. case Ms of
  904. [] ->
  905. State;
  906. _ ->
  907. State2 = update_config(State),
  908. lists:foldl(
  909. fun(QEmail, St) ->
  910. update_retry(QEmail),
  911. spawn_send(QEmail#email_queue.id,
  912. QEmail#email_queue.recipient,
  913. QEmail#email_queue.email,
  914. z_context:depickle(QEmail#email_queue.pickled_context),
  915. St)
  916. end,
  917. State2, Ms)
  918. end;
  919. false ->
  920. State
  921. end.
  922. %% @doc Sets the next retry time for an e-mail.
  923. update_retry(QEmail=#email_queue{retry=Retry}) ->
  924. Period = period(Retry),
  925. Tr = fun()->
  926. mnesia:write(QEmail#email_queue{retry=Retry+1,
  927. retry_on=inc_timestamp(os:timestamp(), Period)})
  928. end,
  929. mnesia:transaction(Tr).
  930. period(0) -> 10;
  931. period(1) -> 60;
  932. period(2) -> 12 * 60;
  933. period(_) -> 24 * 60. % Retry every day for extreme cases
  934. %% @doc Increases a timestamp (as returned by now/0) with a value provided in minutes
  935. inc_timestamp({MegaSec, Sec, MicroSec}, MinToAdd) when is_integer(MinToAdd) ->
  936. Sec2 = Sec + (MinToAdd * 60),
  937. Sec3 = Sec2 rem 1000000,
  938. MegaSec2 = MegaSec + Sec2 div 1000000,
  939. {MegaSec2, Sec3, MicroSec}.
  940. %% @doc Check if an e-mail address is valid
  941. is_valid_email(Recipient) ->
  942. Recipient1 = string:strip(z_string:line(binary_to_list(z_convert:to_binary(Recipient)))),
  943. {_RcptName, RecipientEmail} = z_email:split_name_email(Recipient1),
  944. case re:run(RecipientEmail, [$^|re()]++"$", [extended]) of
  945. nomatch -> false;
  946. {match,_} -> true
  947. end.
  948. re() ->
  949. "(
  950. (\"[^\"\\f\\n\\r\\t\\v\\b]+\")
  951. | ([\\w\\!\\#\\$\\%\\&\'\\*\\+\\-\\~\\/\\^\\`\\|\\{\\}]+
  952. (\\.[\\w\\!\\#\\$\\%\\&\\'\\*\\+\\-\\~\\/\^\`\\|\\{\\}]+)*
  953. )
  954. )
  955. @
  956. (
  957. (
  958. ([A-Za-z0-9\\-])+\\.
  959. )+
  960. [A-Za-z\\-]{2,}
  961. )".
  962. %% @doc Simple header encoding.
  963. encode_header({Header, [V|Vs]}) when is_list(V) ->
  964. Hdr = lists:map(fun ({K, Value}) when is_list(K), is_list(Value) ->
  965. K ++ "=" ++ Value;
  966. ({K, Value}) when is_atom(K), is_list(Value) ->
  967. atom_to_list(K) ++ "=" ++ Value;
  968. (Value) when is_list(Value) -> Value
  969. end,
  970. [V|Vs]),
  971. Header ++ ": " ++ string:join(Hdr, ";\r\n ");
  972. encode_header({Header, Value})
  973. when Header =:= "To"; Header =:= "From"; Header =:= "Reply-To";
  974. Header =:= "Cc"; Header =:= "Bcc"; Header =:= "Date";
  975. Header =:= "Content-Type"; Header =:= "Mime-Version"; Header =:= "MIME-Version";
  976. Header =:= "Content-Transfer-Encoding" ->
  977. Value1 = lists:filter(fun(H) -> H >= 32 andalso H =< 126 end, Value),
  978. Header ++ ": " ++ Value1;
  979. encode_header({Header, Value}) when is_list(Header), is_list(Value) ->
  980. % Encode all other headers according to rfc2047
  981. Header ++ ": " ++ rfc2047:encode(Value);
  982. encode_header({Header, Value}) when is_atom(Header), is_list(Value) ->
  983. atom_to_list(Header) ++ ": " ++ rfc2047:encode(Value).
  984. encode_headers(Headers) ->
  985. string:join(lists:map(fun encode_header/1, Headers), "\r\n").
  986. %% @doc Copy all tempfiles in the #mail attachments, to prevent automatic deletion while
  987. %% the email is queued.
  988. copy_attachments(#email{attachments=[]} = Email) ->
  989. Email;
  990. copy_attachments(#email{attachments=Atts} = Email) ->
  991. Atts1 = [ copy_attachment(Att) || Att <- Atts ],
  992. Email#email{attachments=Atts1}.
  993. copy_attachment(#upload{tmpfile=File} = Upload) when is_binary(File) ->
  994. copy_attachment(Upload#upload{tmpfile=binary_to_list(File)});
  995. copy_attachment(#upload{tmpfile=File} = Upload) when is_list(File) ->
  996. case filename:extension(File) of
  997. ?TMPFILE_EXT ->
  998. Upload;
  999. _Other ->
  1000. case z_tempfile:is_tempfile(File) of
  1001. true ->
  1002. NewFile = tempfile(),
  1003. {ok, _Size} = file:copy(File, NewFile),
  1004. Upload#upload{tmpfile=NewFile};
  1005. false ->
  1006. Upload
  1007. end
  1008. end;
  1009. copy_attachment(Att) ->
  1010. Att.
  1011. opt_dkim(Context) ->
  1012. z_email_dkim:mimemail_options(Context).