aboutsummaryrefslogtreecommitdiff
path: root/src/mail.c
blob: f72a7972315549ca73f2d6e42af6e5c72b421dc6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#include "mail.h"

#include <curl/curl.h>
#include <stdio.h>
#include <string.h>
#include <time.h>

#include "log.h"

#define URI_MAX_LENGTH 255
#define NEWLINE "\r\n"
#define HEADER_LINE_BUFFER_LENGTH 256

typedef struct message_payload_t {
  FILE* file;
  size_t bytes_sent;
  size_t total_length;
} message_payload_t;

static void write_header_line(FILE* file, const char* name, char* value) {
  char buffer[HEADER_LINE_BUFFER_LENGTH];
  size_t len = snprintf(&buffer[0], HEADER_LINE_BUFFER_LENGTH - 1,
                        "%s: %s" NEWLINE, name, value);
  fwrite(&buffer[0], sizeof(char), len, file);
}

static void write_empty_line(FILE* file) {
  fwrite(NEWLINE NEWLINE, sizeof(char), (sizeof(NEWLINE) - 1) * 2, file);
}

static void write_body_line(FILE* file, char* contents) {
  size_t len = strnlen(contents, RSSMAIL_POST_MAX_FIELD_LENGTH);
  fwrite(contents, sizeof(char), len, file);
}

static void write_post_body(FILE* file, post_item_t* post) {
  write_body_line(file, post->title);
  write_empty_line(file);
  write_body_line(file, post->description);
  write_empty_line(file);
  write_body_line(file, post->url);
}

static void write_post_count(FILE* file, int count) {
  char buffer[128];
  size_t len =
      snprintf(buffer, sizeof(buffer),
               "This message contains %d posts in a digest format assembled by "
               "RSSMail." NEWLINE NEWLINE,
               count);
  fwrite(&buffer[0], sizeof(char), len, file);
}

static void write_divider(FILE* file) {
  char buffer[] = NEWLINE NEWLINE NEWLINE "-----" NEWLINE NEWLINE NEWLINE;
  fwrite(&buffer[0], sizeof(char), sizeof(buffer) - 1, file);
}

static void build_digest_body(FILE* file, post_item_t* posts, int count) {
  write_post_count(file, count);

  // write the first post (isn't followed by divider line)
  write_post_body(file, &posts[0]);

  // if there was only one post, we're done
  if (count == 1) {
    return;
  }

  // write the rest of the posts
  for (int i = 1; i < count; ++i) {
    write_divider(file);
    write_post_body(file, &posts[i]);
  }
}

static void build_digest_headers(FILE* file, smtp_config_t* config,
                                 int post_count) {
  // get a local timestamp
  time_t ticks = time(NULL);
  struct tm* now = localtime(&ticks);
  char date_buffer[HEADER_LINE_BUFFER_LENGTH];
  strftime(&date_buffer[0], HEADER_LINE_BUFFER_LENGTH, "%c", now);

  // write headers
  write_header_line(file, "Date", &date_buffer[0]);
  write_header_line(file, "To", config->to);
  write_header_line(file, "From", config->from);
  write_header_line(file, "MIME-Version", "1.0");
  write_header_line(file, "Content-Transfer-Encoding", "8bit");
  write_header_line(file, "Content-Type", "text/plain;charset=utf-8");

  char subject_buffer[HEADER_LINE_BUFFER_LENGTH];
  snprintf(&subject_buffer[0], HEADER_LINE_BUFFER_LENGTH, "%s (%d posts)",
           config->subject, post_count);

  write_header_line(file, "Subject", &subject_buffer[0]);

  // terminate with an additional cr/lf
  write_empty_line(file);
}

static FILE* build_digest_payload(post_item_t* posts, int count, size_t* length,
                                  smtp_config_t* config) {
#ifdef RSSMAIL_SEND_TO_FILE
  FILE* file = fopen("rssmail_output", "w+");
#else
  FILE* file = tmpfile();
#endif

  build_digest_headers(file, config, count);
  build_digest_body(file, posts, count);

  // add null-terminator
  // fwrite("\0", sizeof(char), 1, file);

  *length = (size_t)ftell(file);

  LOG_DEBUG("Digest payload size: %zu bytes", *length);

  fseek(file, 0, SEEK_SET);
  return file;
}

static void free_digest_payload(message_payload_t* payload) {
  if (payload != NULL) {
    if (payload->file != NULL) {
      fclose(payload->file);
    }

    free(payload);
  }
}

static size_t upload_payload(char* ptr, size_t size, size_t count,
                             void* userdata) {
  message_payload_t* payload = (message_payload_t*)userdata;
  if (payload->bytes_sent >= payload->total_length || size * count == 0 ||
      feof(payload->file) != 0) {
    LOG_DEBUG("Reached the end of the payload");
    return 0;
  }

  size_t bytes_to_send = count * size;
  size_t bytes_remaining = payload->total_length - payload->bytes_sent;
  if (bytes_to_send > bytes_remaining) {
    bytes_to_send = bytes_remaining;
  }

  size_t bytes_sent = fread(ptr, 1, bytes_to_send, payload->file);

  payload->bytes_sent += bytes_sent;

  LOG_DEBUG("Sent %zu of %zu bytes to the mail server", payload->bytes_sent,
            payload->total_length);

  return bytes_sent;
}

int send_posts_as_digest(smtp_config_t* config, post_item_t* posts, int count) {
  CURL* curl = NULL;
  struct curl_slist* recipients = NULL;
  message_payload_t* payload = NULL;

  char smtp_uri[URI_MAX_LENGTH + 1];
  if (snprintf(&smtp_uri[0], URI_MAX_LENGTH, "smtps://%s:%d", config->host,
               config->port) >= URI_MAX_LENGTH) {
    LOG_ERROR(
        "The provided SMTP URI is too long; the maximum allowed length is %d",
        URI_MAX_LENGTH);
    return RSSMAIL_SEND_FAILURE;
  }

  if ((curl = curl_easy_init()) == NULL) {
    LOG_ERROR("Failed to create CURL prior to sending mail digest");
    return RSSMAIL_SEND_FAILURE;
  }

  for (int i = 0; i < config->recipient_count; ++i) {
    recipients = curl_slist_append(recipients, config->recipients[i]);
  }

  payload = (message_payload_t*)malloc(sizeof(message_payload_t));
  payload->bytes_sent = 0;
  payload->file =
      build_digest_payload(posts, count, &payload->total_length, config);

  curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients);
  curl_easy_setopt(curl, CURLOPT_MAIL_FROM, config->from);
  curl_easy_setopt(curl, CURLOPT_USERNAME, config->username);
  curl_easy_setopt(curl, CURLOPT_PASSWORD, config->password);
  curl_easy_setopt(curl, CURLOPT_URL, &smtp_uri[0]);
  curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL);
  curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
  curl_easy_setopt(curl, CURLOPT_READFUNCTION, upload_payload);
  curl_easy_setopt(curl, CURLOPT_READDATA, payload);
  curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);

  if (config->cert_path != NULL) {
    curl_easy_setopt(curl, CURLOPT_CAINFO, config->cert_path);
  }

#ifdef DEBUG
  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
#endif

#ifndef RSSMAIL_SEND_TO_FILE
  CURLcode result = curl_easy_perform(curl);
  if (result != CURLE_OK) {
    LOG_ERROR("Failed to send message: %s", curl_easy_strerror(result));
  }
#endif

  free_digest_payload(payload);

  curl_slist_free_all(recipients);

  curl_easy_cleanup(curl);

  return RSSMAIL_SEND_SUCCESS;
}