Edit

kc3-lang/smtp/src/mailx.c

Branch :

  • Show log

    Commit

  • Author : humphreyj
    Date : 2018-06-05 14:37:13
    Hash : 65bec04b
    Message : Read the email body from stdin.

  • src/mailx.c
  • /**
     * @file
     * @brief POSIX mailx utility.
     * @author James Humphrey (mail@somnisoft.com)
     * @version 0.99
     *
     * Implementation of POSIX mailx utility in send mode.
     *
     * mailx [-s subject] [[-S option]...] [[-a attachment]...] address...
     *
     * This software has been placed into the public domain using CC0.
     */
    
    /**
     * Required on some POSIX systems to include some standard functions.
     */
    #define _POSIX_C_SOURCE 200809L
    
    #include <err.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    #include "smtp.h"
    
    /**
     * Stores the to and from email addresses.
     */
    struct mailx_address{
      /**
       * See @ref smtp_address_type.
       */
      enum smtp_address_type address_type;
    
      /**
       * Email address.
       */
      char email[1000];
    };
    
    /**
     * The attachment name and path stored for each attachment to send to the
     * recipient.
     */
    struct mailx_attachment{
      /**
       * File name for this attachment to display to the recipient.
       */
      char name[1000];
    
      /**
       * Local file path pointing to the attachment to send.
       */
      char path[1000];
    };
    
    /**
     * The mailx context structure containing the parameters for setting up the
     * SMTP connection and sending the email.
     */
    struct mailx{
      /**
       * SMTP client context.
       */
      struct smtp *smtp;
    
      /**
       * Email subject line.
       */
      const char *subject;
    
      /**
       * Email body text.
       */
      char *body;
    
      /**
       * SMTP server name or IP address.
       */
      char *server;
    
      /**
       * SMTP server port number.
       */
      char *port;
    
      /**
       * SMTP account user name used for authenticating.
       */
      char *user;
    
      /**
       * SMTP account password used for authenticating.
       */
      char *pass;
    
      /**
       * From email address or name.
       */
      char *from;
    
      /**
       * Determine if using a TLS encrypted connection or plain socket.
       */
      enum smtp_connection_security connection_security;
    
      /**
       * SMTP user account authentication method.
       */
      enum smtp_authentication_method auth_method;
    
      /**
       * Miscellaneous control flags for smtp-lib.
       *
       * See @ref smtp_flag for more details.
       */
      enum smtp_flag smtp_flags;
    
      /**
       * List of email addresses to send to.
       */
      struct mailx_address *address_list;
    
      /**
       * Number of email addresses in @ref address_list.
       */
      size_t num_address;
    
      /**
       * List of files to attach in the email.
       */
      struct mailx_attachment *attachment_list;
    
      /**
       * Number of attachments in @ref attachment_list.
       */
      size_t num_attachment;
    };
    
    /**
     * Read the entire contents of a file stream and store the data into a
     * dynamically allocated buffer.
     *
     * @param[in]  stream     File stream already opened by the caller.
     * @param[out] bytes_read Number of bytes stored in the return buffer.
     * @retval char* A dynamically allocated buffer which contains the entire
     *               contents of @p stream. The caller must free this memory
     *               when done.
     * @retval NULL Memory allocation or file read error.
     */
    static char *
    smtp_ffile_get_contents(FILE *stream,
                            size_t *bytes_read){
      char *read_buf;
      size_t bufsz;
      char *new_buf;
      const size_t BUFSZ_INCREMENT = 512;
    
      read_buf = NULL;
      bufsz = 0;
    
      if(bytes_read){
        *bytes_read = 0;
      }
    
      do{
        size_t bytes_read_loop;
        if((new_buf = realloc(read_buf, bufsz + BUFSZ_INCREMENT)) == NULL){
          free(read_buf);
          return NULL;
        }
        read_buf = new_buf;
        bufsz += BUFSZ_INCREMENT;
    
        bytes_read_loop = fread(&read_buf[bufsz - BUFSZ_INCREMENT],
                                sizeof(char),
                                BUFSZ_INCREMENT,
                                stream);
        if(bytes_read){
          *bytes_read += bytes_read_loop;
        }
        if(ferror(stream)){
          free(read_buf);
          return NULL;
        }
      } while(!feof(stream));
    
      return read_buf;
    }
    
    /**
     * Append this email to the list of email addresses to send to.
     *
     * @param[in] mailx        Append the email address into this mailx context.
     * @param[in] address_type See @ref smtp_address_type.
     * @param[in] email        Email address to send to.
     */
    static void
    mailx_address_append(struct mailx *const mailx,
                         enum smtp_address_type address_type,
                         const char *const email){
      struct mailx_address *new_address;
      size_t new_address_list_sz;
    
      mailx->num_address += 1;
      new_address_list_sz = mailx->num_address * sizeof(*mailx->address_list);
      if((mailx->address_list = realloc(mailx->address_list,
                                     new_address_list_sz)) == NULL){
        err(1, "realloc");
      }
    
      new_address = &mailx->address_list[mailx->num_address - 1];
      new_address->address_type = address_type;
      strncpy(new_address->email, email, sizeof(new_address->email));
      new_address->email[sizeof(new_address->email) - 1] = '\0';
    }
    
    /**
     * Send the email using the configuration options in the @p mailx context.
     *
     * @param[in] mailx Email context.
     */
    static void
    mailx_send(struct mailx *const mailx){
      int rc;
      size_t i;
      const struct mailx_address *address;
      const struct mailx_attachment *attachment;
    
      smtp_open(mailx->server,
                mailx->port,
                mailx->connection_security,
                mailx->smtp_flags,
                &mailx->smtp);
    
      smtp_auth(mailx->smtp,
                mailx->auth_method,
                mailx->user,
                mailx->pass);
    
      for(i = 0; i < mailx->num_address; i++){
        address = &mailx->address_list[i];
        smtp_address_add(mailx->smtp, address->address_type, address->email, NULL);
      }
    
      for(i = 0; i < mailx->num_attachment; i++){
        attachment = &mailx->attachment_list[i];
        smtp_attachment_add_path(mailx->smtp, attachment->name, attachment->path);
      }
      smtp_header_add(mailx->smtp, "Subject", mailx->subject);
      smtp_mail(mailx->smtp, mailx->body);
    
      rc = smtp_close(mailx->smtp);
    
      if(rc != SMTP_STATUS_OK){
        errx(1, "%s", smtp_status_code_errstr(rc));
      }
    }
    
    /**
     * Attach a file to the @p mailx context.
     *
     * @param[in] mailx    Store the attachment details into this mailx context.
     * @param[in] filename File name to display to the recipient.
     * @param[in] path     Local path of file to attach.
     */
    static void
    mailx_append_attachment(struct mailx *const mailx,
                            const char *const filename,
                            const char *const path){
      struct mailx_attachment *new_attachment;
      size_t new_attachment_list_sz;
    
      new_attachment_list_sz = (mailx->num_attachment + 1) *
                               sizeof(*mailx->attachment_list);
      if((mailx->attachment_list = realloc(mailx->attachment_list,
                                           new_attachment_list_sz)) == NULL){
        err(1, "realloc: attachment list");
      }
      new_attachment = &mailx->attachment_list[mailx->num_attachment];
      mailx->num_attachment += 1;
    
      strncpy(new_attachment->name, filename, sizeof(new_attachment->name));
      new_attachment->name[sizeof(new_attachment->name) - 1] = '\0';
    
      strncpy(new_attachment->path, path, sizeof(new_attachment->path));
      new_attachment->path[sizeof(new_attachment->path) - 1] = '\0';
    }
    
    /**
     * Parse the file name and path and attach it to the @p mailx context.
     *
     * @param[in] mailx      Store the attachment details into this mailx context.
     * @param[in] attach_arg String with format: 'filename:filepath'.
     */
    static void
    mailx_append_attachment_arg(struct mailx *const mailx,
                                const char *const attach_arg){
      char *attach_arg_dup;
      char *filename;
      char *filepath;
    
      if((attach_arg_dup = strdup(attach_arg)) == NULL){
        err(1, "strdup: %s", attach_arg);
      }
    
      filename = strtok(attach_arg_dup, ":");
      filepath = strtok(NULL, ":");
    
      mailx_append_attachment(mailx, filename, filepath);
    
      free(attach_arg_dup);
    }
    
    /**
     * Parses the -S option which contains a key/value pair separated by an '='
     * character.
     *
     * @param[in] mailx  Store the results of the option parsing into the relevant
     *                   field in this mailx context.
     * @param[in] option String containing key/value option to parse.
     */
    static void
    mailx_parse_smtp_option(struct mailx *const mailx,
                            const char *const option){
      char *optdup;
      char *opt_key;
      char *opt_value;
      int rc;
    
      rc = 0;
    
      if((optdup = strdup(option)) == NULL){
        err(1, "strdup: option: %s", option);
      }
    
      if((opt_key = strtok(optdup, "=")) == NULL){
        errx(1, "strtok: %s", optdup);
      }
    
      opt_value = strtok(NULL, "=");
    
      if(strcmp(opt_key, "smtp-security") == 0){
        if(strcmp(opt_value, "none") == 0){
          mailx->connection_security = SMTP_SECURITY_NONE;
        }
        else if(strcmp(opt_value, "tls") == 0){
          mailx->connection_security = SMTP_SECURITY_TLS;
        }
        else if(strcmp(opt_value, "starttls") == 0){
          mailx->connection_security = SMTP_SECURITY_STARTTLS;
        }
        else{
          rc = -1;
        }
      }
      else if(strcmp(opt_key, "smtp-auth") == 0){
        if(strcmp(opt_value, "none") == 0){
          mailx->auth_method = SMTP_AUTH_NONE;
        }
        else if(strcmp(opt_value, "plain") == 0){
          mailx->auth_method = SMTP_AUTH_PLAIN;
        }
        else if(strcmp(opt_value, "login") == 0){
          mailx->auth_method = SMTP_AUTH_LOGIN;
        }
        else if(strcmp(opt_value, "cram-md5") == 0){
          mailx->auth_method = SMTP_AUTH_CRAM_MD5;
        }
        else{
          rc = -1;
        }
      }
      else if(strcmp(opt_key, "smtp-flag") == 0){
        if(strcmp(opt_value, "debug") == 0){
          mailx->smtp_flags |= SMTP_DEBUG;
        }
        else if(strcmp(opt_value, "no-cert-verify") == 0){
          mailx->smtp_flags |= SMTP_NO_CERT_VERIFY;
        }
        else{
          rc = -1;
        }
      }
      else if(strcmp(opt_key, "smtp-server") == 0){
        if((mailx->server = strdup(opt_value)) == NULL){
          err(1, "strdup");
        }
      }
      else if(strcmp(opt_key, "smtp-port") == 0){
        if((mailx->port = strdup(opt_value)) == NULL){
          err(1, "strdup");
        }
      }
      else if(strcmp(opt_key, "smtp-user") == 0){
        if((mailx->user = strdup(opt_value)) == NULL){
          err(1, "strdup");
        }
      }
      else if(strcmp(opt_key, "smtp-pass") == 0){
        if((mailx->pass = strdup(opt_value)) == NULL){
          err(1, "strdup");
        }
      }
      else if(strcmp(opt_key, "smtp-from") == 0){
        if((mailx->from = strdup(opt_value)) == NULL){
          err(1, "strdup");
        }
      }
      else{
        rc = -1;
      }
    
      free(optdup);
    
      if(rc < 0){
        errx(1, "invalid argument: %s", option);
      }
    }
    
    /**
     * Initialize and set the default options in the mailx context.
     *
     * See description of -S argument in main for more details.
     *
     * @param[in] mailx The mailx content to initialize.
     */
    static void
    mailx_init_default_values(struct mailx *const mailx){
      memset(mailx, 0, sizeof(*mailx));
      mailx->subject = "";
      mailx->connection_security = SMTP_SECURITY_NONE;
      mailx->auth_method = SMTP_AUTH_NONE;
    }
    
    /**
     * Frees the allocated memory associated with the mailx context.
     *
     * @param[in] mailx The mailx context to free.
     */
    static void
    mailx_free(const struct mailx *const mailx){
      free(mailx->body);
      free(mailx->server);
      free(mailx->port);
      free(mailx->user);
      free(mailx->pass);
      free(mailx->from);
    }
    
    /**
     * Main program entry point for the mailx utility.
     *
     * This program supports the following options:
     *   - -a 'name:path' - Attach a file with name to display to recipient and
     *                      file path pointing to file location on local storage.
     *   - -s subject     - Email subject line.
     *   - -S key=value   - A key/value pair to set various configuration options,
     *                      controlling the behavior of the SMTP client connection.
     *
     * The following list contains possible options for the -S argument:
     *   - smtp-security - none, tls, starttls
     *   - smtp-auth     - none, plain, login, cram-md5
     *   - smtp-flag     - debug, no-cert-verify
     *   - smtp-server   - server name or IP address
     *   - smtp-port     - server port number
     *   - smtp-user     - server authentication user name
     *   - smtp-pass     - server authentication user password
     *
     * The following list shows the default option for -S argument if not provided:
     *   - smtp-security - none
     *   - smtp-auth     - none
     *   - smtp-flag     - none
     *   - smtp-server   - localhost
     *   - smtp-port     - 25
     *   - smtp-user     - none
     *   - smtp-pass     - none
     *
     * @param[in] argc Number of arguments in @p argv.
     * @param[in] argv String array containing the program name and any optional
     *                 parameters described above.
     * @retval 0 Email has been sent.
     * @retval 1 An error occurred while sending email. Although unlikely, an email
     *           can still get sent even after returning with this error code.
     */
    int main(int argc, char *argv[]){
      int rc;
      int i;
      struct mailx mailx;
    
      mailx_init_default_values(&mailx);
    
      while((rc = getopt(argc, argv, "s:S:")) != -1){
        switch(rc){
        case 'a':
          mailx_append_attachment_arg(&mailx, optarg);
          break;
        case 's':
          mailx.subject = optarg;
          break;
        case 'S':
          mailx_parse_smtp_option(&mailx, optarg);
          break;
        default:
          return 1;
        }
      }
      argc -= optind;
      argv += optind;
    
      if(argc < 1){
        errx(1, "must provide at least one email destination address");
      }
    
      if(mailx.from == NULL){
        errx(1, "must provide a FROM address");
      }
    
      if(mailx.server == NULL){
        if((mailx.server = strdup("localhost")) == NULL){
          err(1, "strdup");
        }
      }
    
      if(mailx.port == NULL){
        if((mailx.port = strdup("25")) == NULL){
          err(1, "strdup");
        }
      }
    
      if((mailx.body = smtp_ffile_get_contents(stdin, NULL)) == NULL){
        err(1, "failed to read email body from stdin");
      }
    
      mailx_address_append(&mailx, SMTP_ADDRESS_FROM, mailx.from);
    
      for(i = 0; i < argc; i++){
        mailx_address_append(&mailx, SMTP_ADDRESS_TO, argv[i]);
      }
    
      mailx_send(&mailx);
      mailx_free(&mailx);
      return 0;
    }