PageRenderTime 95ms CodeModel.GetById 12ms app.highlight 72ms RepoModel.GetById 1ms app.codeStats 0ms

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