/* GNU Mailutils -- a suite of utilities for electronic mail Copyright (C) 2003-2021 Free Software Foundation, Inc. GNU Mailutils is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Mailutils is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Mailutils. If not, see . */ /* MH send command */ #include #include #include #include #include static char prog_doc[] = N_("Send messages"); static char args_doc[] = N_("FILE [FILE...]"); static const char *draftfolder; /* Use this draft folder */ static int use_draftfolder = 1; static int use_draft; static int append_msgid; /* Append Message-ID: header */ static int background; /* Operate in the background */ static int split_message; /* Split the message */ static unsigned long split_interval; /* Interval in seconds between sending two successive partial messages */ static size_t split_size = 76*632; /* Size of split parts */ static int verbose; /* Produce verbose diagnostics */ static int watch; /* Watch the delivery process */ static int keep_files; /* Keep draft files */ #define DEFAULT_USER_AGENT "MH (" PACKAGE_STRING ")" #define WATCH(c) do {\ if (watch)\ watch_printf c;\ } while (0) static int add_file (char const *name); static void mesg_list_fixup (void); static void add_alias (struct mu_parseopt *po, struct mu_option *opt, char const *arg) { mh_alias_read (arg, 1); } static void set_draftfolder (struct mu_parseopt *po, struct mu_option *opt, char const *arg) { draftfolder = mu_strdup (arg); use_draftfolder = 1; } static void add_draftmessage (struct mu_parseopt *po, struct mu_option *opt, char const *arg) { add_file (arg); } static void set_split_opt (struct mu_parseopt *po, struct mu_option *opt, char const *arg) { char *errmsg; int rc = mu_str_to_c (arg, opt->opt_type, opt->opt_ptr, &errmsg); if (rc) { char const *errtext; if (errmsg) errtext = errmsg; else errtext = mu_strerror (rc); mu_parseopt_error (po, "%s%s: %s", po->po_long_opt_start, opt->opt_long, errtext); free (errmsg); if (!(po->po_flags & MU_PARSEOPT_NO_ERREXIT)) exit (po->po_exit_error); } split_message = 1; } static struct mu_option options[] = { { "alias", 0, N_("FILE"), MU_OPTION_DEFAULT, N_("specify additional alias file"), mu_c_string, NULL, add_alias }, { "draft", 0, NULL, MU_OPTION_DEFAULT, N_("use prepared draft"), mu_c_bool, &use_draft }, { "draftfolder", 0, N_("FOLDER"), MU_OPTION_DEFAULT, N_("specify the folder for message drafts"), mu_c_string, NULL, set_draftfolder }, { "draftmessage", 0, N_("MSG"), MU_OPTION_DEFAULT, N_("use MSG from the draftfolder as a draft"), mu_c_string, NULL, add_draftmessage }, { "nodraftfolder", 0, NULL, MU_OPTION_DEFAULT, N_("undo the effect of the last --draftfolder option"), mu_c_int, &use_draftfolder, NULL, "1" }, { "filter", 0, N_("FILE"), MU_OPTION_HIDDEN, N_("use filter FILE to preprocess the body of the message"), mu_c_string, NULL, mh_opt_notimpl }, { "nofilter", 0, NULL, MU_OPTION_HIDDEN, N_("undo the effect of the last --filter option"), mu_c_int, NULL, mh_opt_notimpl }, { "format", 0, NULL, MU_OPTION_HIDDEN, N_("reformat To: and Cc: addresses"), mu_c_bool, NULL, mh_opt_notimpl_warning }, { "noformat", 0, NULL, MU_OPTION_HIDDEN }, { "forward", 0, NULL, MU_OPTION_HIDDEN, N_("in case of failure forward the draft along with the failure notice to the sender"), mu_c_bool, NULL, mh_opt_notimpl_warning }, { "noforward", 0, NULL, MU_OPTION_HIDDEN, "" }, { "mime", 0, NULL, MU_OPTION_HIDDEN, N_("use MIME encapsulation"), mu_c_bool, NULL, mh_opt_notimpl_warning }, { "msgid", 0, NULL, MU_OPTION_DEFAULT, N_("add Message-ID: field"), mu_c_bool, &append_msgid }, { "push", 0, NULL, MU_OPTION_DEFAULT, N_("run in the background"), mu_c_bool, &background }, { "preserve", 0, NULL, MU_OPTION_DEFAULT, N_("keep draft files"), mu_c_bool, &keep_files }, { "keep", 0, NULL, MU_OPTION_ALIAS }, { "split", 0, N_("SECONDS"), MU_OPTION_DEFAULT, N_("split the draft into several partial messages and send them with SECONDS interval"), mu_c_ulong, &split_interval, set_split_opt }, { "chunksize", 0, N_("NUMBER"), MU_OPTION_DEFAULT, N_("set the size of chunk for --split (in bytes)"), mu_c_size, &split_size }, { "verbose", 0, NULL, MU_OPTION_DEFAULT, N_("print the transcript of interactions with the transport system"), mu_c_bool, &verbose }, { "watch", 0, NULL, MU_OPTION_DEFAULT, N_("monitor the delivery of mail"), mu_c_bool, &watch }, { "width", 0, N_("NUMBER"), MU_OPTION_HIDDEN, N_("make header fields no longer than NUMBER columns"), mu_c_uint, NULL, mh_opt_notimpl_warning }, MU_OPTION_END }; static void watch_printf (const char *fmt, ...) { va_list ap; va_start (ap, fmt); vfprintf (stderr, fmt, ap); fprintf (stderr, "\n"); va_end (ap); } struct list_elt /* Element of the send list */ { const char *file_name; /* Duplicated in msg stream, but there's no way to get it from there */ mu_message_t msg; /* Corresponding message */ }; static mu_list_t mesg_list; static mu_property_t mts_profile; int add_file (char const *name) { struct list_elt *elt; if (!mesg_list && mu_list_create (&mesg_list)) { mu_error (_("cannot create message list")); return 1; } elt = mu_alloc (sizeof *elt); elt->file_name = name; elt->msg = NULL; return mu_list_append (mesg_list, elt); } int checkdraft (const char *name) { struct stat st; if (stat (name, &st)) { mu_error (_("unable to stat draft file %s: %s"), name, mu_strerror (errno)); return 1; } return 0; } int elt_fixup (void *item, void *data) { struct list_elt *elt = item; elt->file_name = mh_expand_name (draftfolder, elt->file_name, NAME_ANY); if (checkdraft (elt->file_name)) exit (1); elt->msg = mh_file_to_message (NULL, elt->file_name); if (!elt->msg) return MU_ERR_USER0; return 0; } void mesg_list_fixup () { if (mesg_list && mu_list_foreach (mesg_list, elt_fixup, NULL)) exit (1); } void read_mts_profile () { char *name; const char *p; char *hostname = NULL; int rc; name = getenv ("MTSTAILOR"); if (name) mts_profile = mh_read_property_file (name, 1); else { mu_property_t local_profile; name = mh_expand_name (MHLIBDIR, "mtstailor", NAME_ANY); mts_profile = mh_read_property_file (name, 1); name = mu_tilde_expansion ("~/.mtstailor", MU_HIERARCHY_DELIMITER, NULL); local_profile = mh_read_property_file (name, 1); mh_property_merge (mts_profile, local_profile); mu_property_destroy (&local_profile); } rc = mu_property_aget_value (mts_profile, "localname", &hostname); if (rc == MU_ERR_NOENT) { rc = mu_get_host_name (&hostname); if (rc) { mu_error (_("cannot get system host name: %s"), mu_strerror (rc)); exit (1); } } else if (rc) { mu_diag_funcall (MU_DIAG_ERROR, "mu_profile_aget_value", "localname", rc); exit (1); } rc = mu_property_sget_value (mts_profile, "localdomain", &p); if (rc == 0) { hostname = mu_realloc (hostname, strlen (hostname) + 1 + strlen (p) + 1); strcat (hostname, "."); strcat (hostname, p); } else if (rc != MU_ERR_NOENT) { mu_diag_funcall (MU_DIAG_ERROR, "mu_profile_sget_value", "localdomain", rc); exit (1); } rc = mu_set_user_email_domain (hostname); free (hostname); if (rc) { mu_error (_("cannot set user mail domain: %s"), mu_strerror (rc)); exit (1); } rc = mu_property_sget_value (mts_profile, "username", &p); if (rc == 0) { size_t len; const char *domain; char *newemail; int rc; rc = mu_get_user_email_domain (&domain); if (rc) { mu_error (_("cannot get user email: %s"), mu_strerror (rc)); exit (1); } len = strlen (p) + 1 + strlen (domain) + 1; newemail = mu_alloc (len); strcpy (newemail, p); strcat (newemail, "@"); strcat (newemail, domain); rc = mu_set_user_email (newemail); if (rc) { mu_error (_("cannot set user email (%s): %s"), newemail, mu_strerror (rc)); exit (1); } free (newemail); } else if (rc != MU_ERR_NOENT) { mu_diag_funcall (MU_DIAG_ERROR, "mu_profile_sget_value", "username", rc); exit (1); } } mu_mailer_t open_mailer () { const char *url = mu_mhprop_get_value (mts_profile, "url", "sendmail:/usr/sbin/sendmail"); mu_mailer_t mailer; int status; WATCH ((_("Creating mailer %s"), url)); status = mu_mailer_create (&mailer, url); if (status) { mu_error (_("cannot create mailer `%s'"), url); return NULL; } if (verbose) { mu_debug_set_category_level (MU_DEBCAT_MAILER, MU_DEBUG_LEVEL_UPTO (MU_DEBUG_PROT)); } WATCH ((_("Opening mailer %s"), url)); status = mu_mailer_open (mailer, MU_STREAM_RDWR); if (status) { mu_error (_("cannot open mailer `%s': %s"), url, mu_strerror (status)); return NULL; } return mailer; } static void create_message_id (mu_header_t hdr) { char *p = mh_create_message_id (0); mu_header_set_value (hdr, MU_HEADER_MESSAGE_ID, p, 1); free (p); } static const char * get_sender_personal () { const char *s = mh_global_profile_get ("signature", getenv ("SIGNATURE")); if (!s) { struct passwd *pw = getpwuid (getuid ()); if (pw && pw->pw_gecos[0]) { char *p = strchr (pw->pw_gecos, ','); if (p) *p = 0; s = pw->pw_gecos; } } return s; } static void set_address_header (mu_header_t hdr, char *name, mu_address_t addr) { const char *value; if (mu_address_sget_printable (addr, &value) == 0) mu_header_set_value (hdr, name, value, 1); /* FIXME: Error reporting */ } void expand_aliases (mu_message_t msg) { mu_header_t hdr; mu_address_t addr_to = NULL, addr_cc = NULL, addr_bcc = NULL; mh_expand_aliases (msg, &addr_to, &addr_cc, &addr_bcc); mu_message_get_header (msg, &hdr); if (addr_to) { set_address_header (hdr, MU_HEADER_TO, addr_to); mu_address_destroy (&addr_to); } if (addr_cc) { set_address_header (hdr, MU_HEADER_CC, addr_cc); mu_address_destroy (&addr_cc); } if (addr_bcc) { set_address_header (hdr, MU_HEADER_BCC, addr_bcc); mu_address_destroy (&addr_bcc); } } void fix_fcc (mu_message_t msg) { mu_header_t hdr; char *fcc; mu_message_get_header (msg, &hdr); if (mu_header_aget_value (hdr, MU_HEADER_FCC, &fcc) == 0) { struct mu_wordsplit ws; int need_fixup = 0; size_t fixup_len = 0; ws.ws_delim = ","; if (mu_wordsplit (fcc, &ws, MU_WRDSF_DEFFLAGS | MU_WRDSF_DELIM | MU_WRDSF_WS)) { mu_error (_("cannot split line `%s': %s"), fcc, mu_wordsplit_strerror (&ws)); } else { size_t i; for (i = 0; i < ws.ws_wordc; i += 2) { if (strchr ("+%~/=", ws.ws_wordv[i][0]) == NULL) { need_fixup++; fixup_len ++; } fixup_len += strlen (ws.ws_wordv[i]); } if (need_fixup) { char *p; /* the new fcc string contains: folder names - fixup_len characters, ws.ws_wordc - 1 comma-space pairs and a terminating nul */ fcc = realloc (fcc, fixup_len + ws.ws_wordc - 1 + 1); for (i = 0, p = fcc; i < ws.ws_wordc; i++) { if (strchr ("+%~/=", ws.ws_wordv[i][0]) == NULL) *p++ = '+'; strcpy (p, ws.ws_wordv[i]); p += strlen (p); *p++ = ','; *p++ = ' '; } *p = 0; } } mu_wordsplit_free (&ws); if (need_fixup) { mu_header_set_value (hdr, MU_HEADER_FCC, fcc, 1); WATCH ((_("Fixed fcc: %s"), fcc)); } free (fcc); } } /* Convert MH-style DCC headers to normal BCC. FIXME: Normally we should iterate through the headers to catch multiple Dcc occurrences (the same holds true for Fcc as well), however at the time of this writing we have mu_header_get_field_value, but we miss mu_header_set_field_value. */ void fix_dcc (mu_message_t msg) { mu_header_t hdr; char *dcc; mu_message_get_header (msg, &hdr); if (mu_header_aget_value (hdr, MU_HEADER_DCC, &dcc) == 0) { char *bcc = NULL; mu_header_set_value (hdr, MU_HEADER_DCC, NULL, 1); mu_header_aget_value (hdr, MU_HEADER_BCC, &bcc); if (bcc) { char *newbcc = realloc (bcc, strlen (bcc) + 1 + strlen (dcc) + 1); if (!newbcc) { mu_error (_("not enough memory")); free (dcc); free (bcc); return; } bcc = newbcc; strcat (bcc, ","); strcat (bcc, dcc); free (dcc); } else bcc = dcc; WATCH ((_("Fixed bcc: %s"), bcc)); mu_header_set_value (hdr, MU_HEADER_BCC, bcc, 1); free (bcc); } } void backup_file (const char *file_name) { char *new_name = mu_alloc (strlen (file_name) + 2); char *p = strrchr (file_name, '/'); if (p) { size_t len = p - file_name + 1; memcpy (new_name, file_name, len); new_name[len++] = ','; strcpy (new_name + len, p + 1); } else { new_name[0] = ','; strcpy (new_name + 1, file_name); } WATCH ((_("Renaming %s to %s"), file_name, new_name)); if (unlink (new_name) && errno != ENOENT) mu_diag_funcall (MU_DIAG_ERROR, "unlink", new_name, errno); else { int rc = mu_rename_file (file_name, new_name, MU_COPY_OVERWRITE); if (rc) mu_error (_("cannot rename %s to %s: %s"), file_name, new_name, mu_strerror (errno)); } free (new_name); } int _action_send (void *item, void *data) { struct list_elt *elt = item; mu_message_t msg = elt->msg; int rc; mu_mailer_t mailer; mu_header_t hdr; size_t n; WATCH ((_("Getting message %s"), elt->file_name)); if (mu_message_get_header (msg, &hdr) == 0) { char date[80]; time_t t = time (NULL); struct tm *tm = localtime (&t); mu_strftime (date, sizeof date, "%a, %d %b %Y %H:%M:%S %z", tm); mu_header_set_value (hdr, MU_HEADER_DATE, date, 1); if (mu_header_get_value (hdr, MU_HEADER_FROM, NULL, 0, &n)) { char *from; char *email = mu_get_user_email (NULL); const char *pers = get_sender_personal (); if (pers) { mu_asprintf (&from, "\"%s\" <%s>", pers, email); free (email); } else from = email; mu_header_set_value (hdr, MU_HEADER_FROM, from, 1); free (from); } if (append_msgid && mu_header_get_value (hdr, MU_HEADER_MESSAGE_ID, NULL, 0, &n)) create_message_id (hdr); if (mu_header_get_value (hdr, MU_HEADER_USER_AGENT, NULL, 0, &n)) { const char *p = mu_mhprop_get_value (mts_profile, "user-agent", mu_mhprop_get_value (mts_profile, "x-mailer", "yes")); if (!strcmp (p, "yes")) mu_header_set_value (hdr, MU_HEADER_USER_AGENT, DEFAULT_USER_AGENT, 0); else if (strcmp (p, "no")) mu_header_remove (hdr, MU_HEADER_USER_AGENT, 1); } } expand_aliases (msg); fix_fcc (msg); fix_dcc (msg); mailer = open_mailer (); if (!mailer) return MU_ERR_FAILURE; WATCH ((_("Sending message %s"), elt->file_name)); if (split_message) { struct timeval delay; delay.tv_sec = split_interval; delay.tv_usec = 0; rc = mu_mailer_send_fragments (mailer, msg, split_size, &delay, NULL, NULL); } else rc = mu_mailer_send_message (mailer, msg, NULL, NULL); if (rc) { mu_error(_("cannot send message: %s"), mu_strerror (rc)); return MU_ERR_FAILURE; } WATCH ((_("Destroying the mailer"))); mu_mailer_close (mailer); mu_mailer_destroy (&mailer); if (!keep_files) backup_file (elt->file_name); return 0; } static int _add_to_mesg_list (size_t num, mu_message_t msg, void *data) { char const *path = data; struct list_elt *elt; size_t uid; int rc; char *file_name; if (!mesg_list && mu_list_create (&mesg_list)) { mu_error (_("cannot create message list")); return 1; } mu_message_get_uid (msg, &uid); file_name = mu_make_file_name (path, mu_umaxtostr (0, uid)); if (!use_draft) { if (!mh_usedraft (file_name)) exit (0); } elt = mu_alloc (sizeof *elt); elt->msg = msg; elt->file_name = file_name; rc = mu_list_append (mesg_list, elt); if (rc) { mu_diag_funcall (MU_DIAG_ERROR, "mu_list_append", NULL, rc); exit (1); } return 0; } static void addfolder (const char *folder, int argc, char **argv) { mu_url_t url; const char *path; mu_msgset_t msgset; mu_mailbox_t mbox = mh_open_folder (folder, MU_STREAM_READ); if (!mbox) { mu_error (_("cannot open folder %s: %s"), folder, mu_strerror (errno)); exit (1); } mh_msgset_parse (&msgset, mbox, argc, argv, "cur"); if (!use_draft) { size_t count = 0; mu_msgset_count (msgset, &count); if (count > 1) use_draft = 1; } mu_mailbox_get_url (mbox, &url); mu_url_sget_path (url, &path); mu_msgset_foreach_message (msgset, _add_to_mesg_list, (void*)path); mu_msgset_free (msgset); } /* Usage cases: * * 1. send * a) If Draft-Folder is set: ask whether to use "cur" message from that * folder as a draft; * b) If Draft-Folder is not set: ask whether to use $(Path)/draft; * 2. send -draft * Use $(Path)/draft * 3. send MSG * Use $(Path)/MSG * 4. send -draftmessage MSG * Same as (3) * 5. send -draftfolder DIR * Use "cur" from that folder * 6. send -draftfolder DIR MSG * Use MSG from folder DIR * 7. send -draftfolder DIR -draftmessage MSG * Same as 6. */ int main (int argc, char **argv) { mu_mailbox_t mbox = NULL; char *p; int rc; mh_getopt (&argc, &argv, options, 0, args_doc, prog_doc, NULL); mh_read_aliases (); /* Process the mtstailor file */ read_mts_profile (); if (!draftfolder) { if (mu_list_is_empty (mesg_list) && argc == 0) { char *dfolder = (!use_draft && use_draftfolder) ? (char*) mh_global_profile_get ("Draft-Folder", NULL) : NULL; if (dfolder) addfolder (dfolder, 0, NULL); else { char *df = mh_expand_name (mu_folder_directory (), "draft", NAME_ANY); if (checkdraft (df)) exit (1); if (!use_draft && !mh_usedraft (df)) exit (0); add_file (df); mesg_list_fixup (); } } else { while (argc--) add_file (*argv++); mesg_list_fixup (); } } else { /* -draftfolder is supplied */ draftfolder = mh_expand_name (mu_folder_directory (), draftfolder, NAME_ANY); use_draft = 1; mesg_list_fixup (); if (mu_list_is_empty (mesg_list) || argc != 0) addfolder (draftfolder, argc, argv); } /* Detach from the console if required */ if (background && (rc = mu_daemon ()) != 0) { mu_error (_("cannot switch to background: %s"), mu_strerror (rc)); return 1; } /* Prepend url specifier to the folder dir. We won't need this when the default format becomes configurable */ mu_asprintf (&p, "mh:%s", mu_folder_directory ()); mu_set_folder_directory (p); free (p); /* Finally, do the work */ rc = mu_list_foreach (mesg_list, _action_send, NULL); mu_mailbox_destroy (&mbox); return !!rc; }