Coverage Report

Created: 2023-06-07 06:15

/src/neomutt/pop/lib.c
Line
Count
Source (jump to first uncovered line)
1
/**
2
 * @file
3
 * POP helper routines
4
 *
5
 * @authors
6
 * Copyright (C) 2000-2003 Vsevolod Volkov <vvv@mutt.org.ua>
7
 * Copyright (C) 2018 Richard Russon <rich@flatcap.org>
8
 *
9
 * @copyright
10
 * This program is free software: you can redistribute it and/or modify it under
11
 * the terms of the GNU General Public License as published by the Free Software
12
 * Foundation, either version 2 of the License, or (at your option) any later
13
 * version.
14
 *
15
 * This program is distributed in the hope that it will be useful, but WITHOUT
16
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18
 * details.
19
 *
20
 * You should have received a copy of the GNU General Public License along with
21
 * this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
24
/**
25
 * @page pop_lib POP helper routines
26
 *
27
 * POP helper routines
28
 */
29
30
#include "config.h"
31
#include <arpa/inet.h>
32
#include <errno.h>
33
#include <netdb.h>
34
#include <stdbool.h>
35
#include <stdio.h>
36
#include <stdlib.h>
37
#include <string.h>
38
#include "private.h"
39
#include "mutt/lib.h"
40
#include "config/lib.h"
41
#include "email/lib.h"
42
#include "core/lib.h"
43
#include "conn/lib.h"
44
#include "question/lib.h"
45
#include "progress/lib.h"
46
#include "adata.h"
47
#include "edata.h"
48
#include "mutt_account.h"
49
#include "mutt_logging.h"
50
51
struct Progress;
52
53
/**
54
 * pop_get_field - Get connection login credentials - Implements ConnAccount::get_field()
55
 */
56
const char *pop_get_field(enum ConnAccountField field, void *gf_data)
57
0
{
58
0
  switch (field)
59
0
  {
60
0
    case MUTT_CA_LOGIN:
61
0
    case MUTT_CA_USER:
62
0
      return cs_subset_string(NeoMutt->sub, "pop_user");
63
0
    case MUTT_CA_PASS:
64
0
      return cs_subset_string(NeoMutt->sub, "pop_pass");
65
0
    case MUTT_CA_OAUTH_CMD:
66
0
      return cs_subset_string(NeoMutt->sub, "pop_oauth_refresh_command");
67
0
    case MUTT_CA_HOST:
68
0
    default:
69
0
      return NULL;
70
0
  }
71
0
}
72
73
/**
74
 * pop_parse_path - Parse a POP mailbox name
75
 * @param path Path to parse
76
 * @param cac  Account to store details
77
 * @retval 0 success
78
 * @retval -1 error
79
 *
80
 * Split a POP path into host, port, username and password
81
 */
82
int pop_parse_path(const char *path, struct ConnAccount *cac)
83
0
{
84
  /* Defaults */
85
0
  cac->flags = 0;
86
0
  cac->type = MUTT_ACCT_TYPE_POP;
87
0
  cac->port = 0;
88
0
  cac->service = "pop";
89
0
  cac->get_field = pop_get_field;
90
91
0
  struct Url *url = url_parse(path);
92
93
0
  if (!url || ((url->scheme != U_POP) && (url->scheme != U_POPS)) ||
94
0
      !url->host || (mutt_account_fromurl(cac, url) < 0))
95
0
  {
96
0
    url_free(&url);
97
0
    mutt_error(_("Invalid POP URL: %s"), path);
98
0
    return -1;
99
0
  }
100
101
0
  if (url->scheme == U_POPS)
102
0
    cac->flags |= MUTT_ACCT_SSL;
103
104
0
  struct servent *service = getservbyname((url->scheme == U_POP) ? "pop3" : "pop3s", "tcp");
105
0
  if (cac->port == 0)
106
0
  {
107
0
    if (service)
108
0
      cac->port = ntohs(service->s_port);
109
0
    else
110
0
      cac->port = (url->scheme == U_POP) ? POP_PORT : POP_SSL_PORT;
111
0
  }
112
113
0
  url_free(&url);
114
0
  return 0;
115
0
}
116
117
/**
118
 * pop_error - Copy error message to err_msg buffer
119
 * @param adata POP Account data
120
 * @param msg   Error message to save
121
 */
122
static void pop_error(struct PopAccountData *adata, char *msg)
123
0
{
124
0
  char *t = strchr(adata->err_msg, '\0');
125
0
  char *c = msg;
126
127
0
  size_t plen = mutt_str_startswith(msg, "-ERR ");
128
0
  if (plen != 0)
129
0
  {
130
0
    char *c2 = mutt_str_skip_email_wsp(msg + plen);
131
132
0
    if (*c2)
133
0
      c = c2;
134
0
  }
135
136
0
  mutt_str_copy(t, c, sizeof(adata->err_msg) - strlen(adata->err_msg));
137
0
  mutt_str_remove_trailing_ws(adata->err_msg);
138
0
}
139
140
/**
141
 * fetch_capa - Parse CAPA output - Implements ::pop_fetch_t - @ingroup pop_fetch_api
142
 * @param line List of capabilities
143
 * @param data POP data
144
 * @retval 0 (always)
145
 */
146
static int fetch_capa(const char *line, void *data)
147
0
{
148
0
  struct PopAccountData *adata = data;
149
150
0
  if (mutt_istr_startswith(line, "SASL"))
151
0
  {
152
0
    const char *c = mutt_str_skip_email_wsp(line + 4);
153
0
    buf_strcpy(&adata->auth_list, c);
154
0
  }
155
0
  else if (mutt_istr_startswith(line, "STLS"))
156
0
  {
157
0
    adata->cmd_stls = true;
158
0
  }
159
0
  else if (mutt_istr_startswith(line, "USER"))
160
0
  {
161
0
    adata->cmd_user = 1;
162
0
  }
163
0
  else if (mutt_istr_startswith(line, "UIDL"))
164
0
  {
165
0
    adata->cmd_uidl = 1;
166
0
  }
167
0
  else if (mutt_istr_startswith(line, "TOP"))
168
0
  {
169
0
    adata->cmd_top = 1;
170
0
  }
171
172
0
  return 0;
173
0
}
174
175
/**
176
 * fetch_auth - Fetch list of the authentication mechanisms - Implements ::pop_fetch_t - @ingroup pop_fetch_api
177
 * @param line List of authentication methods
178
 * @param data POP data
179
 * @retval 0 (always)
180
 */
181
static int fetch_auth(const char *line, void *data)
182
0
{
183
0
  struct PopAccountData *adata = data;
184
185
0
  if (!buf_is_empty(&adata->auth_list))
186
0
  {
187
0
    buf_addstr(&adata->auth_list, " ");
188
0
  }
189
0
  buf_addstr(&adata->auth_list, line);
190
191
0
  return 0;
192
0
}
193
194
/**
195
 * pop_capabilities - Get capabilities from a POP server
196
 * @param adata POP Account data
197
 * @param mode  Initial capabilities
198
 * @retval  0 Successful
199
 * @retval -1 Connection lost
200
 * @retval -2 Execution error
201
 */
202
static int pop_capabilities(struct PopAccountData *adata, int mode)
203
0
{
204
0
  char buf[1024] = { 0 };
205
206
  /* don't check capabilities on reconnect */
207
0
  if (adata->capabilities)
208
0
    return 0;
209
210
  /* init capabilities */
211
0
  if (mode == 0)
212
0
  {
213
0
    adata->cmd_capa = false;
214
0
    adata->cmd_stls = false;
215
0
    adata->cmd_user = 0;
216
0
    adata->cmd_uidl = 0;
217
0
    adata->cmd_top = 0;
218
0
    adata->resp_codes = false;
219
0
    adata->expire = true;
220
0
    adata->login_delay = 0;
221
0
    buf_init(&adata->auth_list);
222
0
  }
223
224
  /* Execute CAPA command */
225
0
  if ((mode == 0) || adata->cmd_capa)
226
0
  {
227
0
    mutt_str_copy(buf, "CAPA\r\n", sizeof(buf));
228
0
    switch (pop_fetch_data(adata, buf, NULL, fetch_capa, adata))
229
0
    {
230
0
      case 0:
231
0
      {
232
0
        adata->cmd_capa = true;
233
0
        break;
234
0
      }
235
0
      case -1:
236
0
        return -1;
237
0
    }
238
0
  }
239
240
  /* CAPA not supported, use defaults */
241
0
  if ((mode == 0) && !adata->cmd_capa)
242
0
  {
243
0
    adata->cmd_user = 2;
244
0
    adata->cmd_uidl = 2;
245
0
    adata->cmd_top = 2;
246
247
0
    mutt_str_copy(buf, "AUTH\r\n", sizeof(buf));
248
0
    if (pop_fetch_data(adata, buf, NULL, fetch_auth, adata) == -1)
249
0
      return -1;
250
0
  }
251
252
  /* Check capabilities */
253
0
  if (mode == 2)
254
0
  {
255
0
    char *msg = NULL;
256
257
0
    if (!adata->expire)
258
0
      msg = _("Unable to leave messages on server");
259
0
    if (adata->cmd_top == 0)
260
0
      msg = _("Command TOP is not supported by server");
261
0
    if (adata->cmd_uidl == 0)
262
0
      msg = _("Command UIDL is not supported by server");
263
0
    if (msg && adata->cmd_capa)
264
0
    {
265
0
      mutt_error(msg);
266
0
      return -2;
267
0
    }
268
0
    adata->capabilities = true;
269
0
  }
270
271
0
  return 0;
272
0
}
273
274
/**
275
 * pop_connect - Open connection
276
 * @param adata POP Account data
277
 * @retval  0 Successful
278
 * @retval -1 Connection lost
279
 * @retval -2 Invalid response
280
 */
281
int pop_connect(struct PopAccountData *adata)
282
0
{
283
0
  char buf[1024] = { 0 };
284
285
0
  adata->status = POP_NONE;
286
0
  if ((mutt_socket_open(adata->conn) < 0) ||
287
0
      (mutt_socket_readln(buf, sizeof(buf), adata->conn) < 0))
288
0
  {
289
0
    mutt_error(_("Error connecting to server: %s"), adata->conn->account.host);
290
0
    return -1;
291
0
  }
292
293
0
  adata->status = POP_CONNECTED;
294
295
0
  if (!mutt_str_startswith(buf, "+OK"))
296
0
  {
297
0
    *adata->err_msg = '\0';
298
0
    pop_error(adata, buf);
299
0
    mutt_error("%s", adata->err_msg);
300
0
    return -2;
301
0
  }
302
303
0
  pop_apop_timestamp(adata, buf);
304
305
0
  return 0;
306
0
}
307
308
/**
309
 * pop_open_connection - Open connection and authenticate
310
 * @param adata POP Account data
311
 * @retval  0 Successful
312
 * @retval -1 Connection lost
313
 * @retval -2 Invalid command or execution error
314
 * @retval -3 Authentication cancelled
315
 */
316
int pop_open_connection(struct PopAccountData *adata)
317
0
{
318
0
  char buf[1024] = { 0 };
319
320
0
  int rc = pop_connect(adata);
321
0
  if (rc < 0)
322
0
    return rc;
323
324
0
  rc = pop_capabilities(adata, 0);
325
0
  if (rc == -1)
326
0
    goto err_conn;
327
0
  if (rc == -2)
328
0
    return -2;
329
330
#ifdef USE_SSL
331
  /* Attempt STLS if available and desired. */
332
  const bool c_ssl_force_tls = cs_subset_bool(NeoMutt->sub, "ssl_force_tls");
333
  if ((adata->conn->ssf == 0) && (adata->cmd_stls || c_ssl_force_tls))
334
  {
335
    if (c_ssl_force_tls)
336
      adata->use_stls = 2;
337
    if (adata->use_stls == 0)
338
    {
339
      const enum QuadOption c_ssl_starttls = cs_subset_quad(NeoMutt->sub, "ssl_starttls");
340
      enum QuadOption ans = query_quadoption(c_ssl_starttls, _("Secure connection with TLS?"));
341
      if (ans == MUTT_ABORT)
342
        return -2;
343
      adata->use_stls = 1;
344
      if (ans == MUTT_YES)
345
        adata->use_stls = 2;
346
    }
347
    if (adata->use_stls == 2)
348
    {
349
      mutt_str_copy(buf, "STLS\r\n", sizeof(buf));
350
      rc = pop_query(adata, buf, sizeof(buf));
351
      // Clear any data after the STLS acknowledgement
352
      mutt_socket_empty(adata->conn);
353
      if (rc == -1)
354
        goto err_conn;
355
      if (rc != 0)
356
      {
357
        mutt_error("%s", adata->err_msg);
358
      }
359
      else if (mutt_ssl_starttls(adata->conn))
360
      {
361
        mutt_error(_("Could not negotiate TLS connection"));
362
        return -2;
363
      }
364
      else
365
      {
366
        /* recheck capabilities after STLS completes */
367
        rc = pop_capabilities(adata, 1);
368
        if (rc == -1)
369
          goto err_conn;
370
        if (rc == -2)
371
          return -2;
372
      }
373
    }
374
  }
375
376
  if (c_ssl_force_tls && (adata->conn->ssf == 0))
377
  {
378
    mutt_error(_("Encrypted connection unavailable"));
379
    return -2;
380
  }
381
#endif
382
383
0
  rc = pop_authenticate(adata);
384
0
  if (rc == -1)
385
0
    goto err_conn;
386
0
  if (rc == -3)
387
0
    mutt_clear_error();
388
0
  if (rc != 0)
389
0
    return rc;
390
391
  /* recheck capabilities after authentication */
392
0
  rc = pop_capabilities(adata, 2);
393
0
  if (rc == -1)
394
0
    goto err_conn;
395
0
  if (rc == -2)
396
0
    return -2;
397
398
  /* get total size of mailbox */
399
0
  mutt_str_copy(buf, "STAT\r\n", sizeof(buf));
400
0
  rc = pop_query(adata, buf, sizeof(buf));
401
0
  if (rc == -1)
402
0
    goto err_conn;
403
0
  if (rc == -2)
404
0
  {
405
0
    mutt_error("%s", adata->err_msg);
406
0
    return rc;
407
0
  }
408
409
0
  unsigned int n = 0, size = 0;
410
0
  sscanf(buf, "+OK %u %u", &n, &size);
411
0
  adata->size = size;
412
0
  return 0;
413
414
0
err_conn:
415
0
  adata->status = POP_DISCONNECTED;
416
0
  mutt_error(_("Server closed connection"));
417
0
  return -1;
418
0
}
419
420
/**
421
 * pop_logout - Logout from a POP server
422
 * @param m Mailbox
423
 */
424
void pop_logout(struct Mailbox *m)
425
0
{
426
0
  struct PopAccountData *adata = pop_adata_get(m);
427
428
0
  if (adata->status == POP_CONNECTED)
429
0
  {
430
0
    int rc = 0;
431
0
    char buf[1024] = { 0 };
432
0
    mutt_message(_("Closing connection to POP server..."));
433
434
0
    if (m->readonly)
435
0
    {
436
0
      mutt_str_copy(buf, "RSET\r\n", sizeof(buf));
437
0
      rc = pop_query(adata, buf, sizeof(buf));
438
0
    }
439
440
0
    if (rc != -1)
441
0
    {
442
0
      mutt_str_copy(buf, "QUIT\r\n", sizeof(buf));
443
0
      rc = pop_query(adata, buf, sizeof(buf));
444
0
    }
445
446
0
    if (rc < 0)
447
0
      mutt_debug(LL_DEBUG1, "Error closing POP connection\n");
448
449
0
    mutt_clear_error();
450
0
  }
451
452
0
  adata->status = POP_DISCONNECTED;
453
0
}
454
455
/**
456
 * pop_query_d - Send data from buffer and receive answer to the same buffer
457
 * @param adata  POP Account data
458
 * @param buf    Buffer to send/store data
459
 * @param buflen Buffer length
460
 * @param msg    Progress message
461
 * @retval  0 Successful
462
 * @retval -1 Connection lost
463
 * @retval -2 Invalid command or execution error
464
 */
465
int pop_query_d(struct PopAccountData *adata, char *buf, size_t buflen, char *msg)
466
0
{
467
0
  if (adata->status != POP_CONNECTED)
468
0
    return -1;
469
470
  /* print msg instead of real command */
471
0
  if (msg)
472
0
  {
473
0
    mutt_debug(MUTT_SOCK_LOG_CMD, "> %s", msg);
474
0
  }
475
476
0
  mutt_socket_send_d(adata->conn, buf, MUTT_SOCK_LOG_FULL);
477
478
0
  char *c = strpbrk(buf, " \r\n");
479
0
  if (c)
480
0
    *c = '\0';
481
0
  snprintf(adata->err_msg, sizeof(adata->err_msg), "%s: ", buf);
482
483
0
  if (mutt_socket_readln_d(buf, buflen, adata->conn, MUTT_SOCK_LOG_FULL) < 0)
484
0
  {
485
0
    adata->status = POP_DISCONNECTED;
486
0
    return -1;
487
0
  }
488
0
  if (mutt_str_startswith(buf, "+OK"))
489
0
    return 0;
490
491
0
  pop_error(adata, buf);
492
0
  return -2;
493
0
}
494
495
/**
496
 * pop_fetch_data - Read Headers with callback function
497
 * @param adata    POP Account data
498
 * @param query    POP query to send to server
499
 * @param progress Progress bar
500
 * @param callback Function called for each header read
501
 * @param data     Data to pass to the callback
502
 * @retval  0 Successful
503
 * @retval -1 Connection lost
504
 * @retval -2 Invalid command or execution error
505
 * @retval -3 Error in callback(*line, *data)
506
 *
507
 * This function calls  callback(*line, *data)  for each received line,
508
 * callback(NULL, *data)  if  rewind(*data)  needs, exits when fail or done.
509
 */
510
int pop_fetch_data(struct PopAccountData *adata, const char *query,
511
                   struct Progress *progress, pop_fetch_t callback, void *data)
512
0
{
513
0
  char buf[1024] = { 0 };
514
0
  long pos = 0;
515
0
  size_t lenbuf = 0;
516
517
0
  mutt_str_copy(buf, query, sizeof(buf));
518
0
  int rc = pop_query(adata, buf, sizeof(buf));
519
0
  if (rc < 0)
520
0
    return rc;
521
522
0
  char *inbuf = mutt_mem_malloc(sizeof(buf));
523
524
0
  while (true)
525
0
  {
526
0
    const int chunk = mutt_socket_readln_d(buf, sizeof(buf), adata->conn, MUTT_SOCK_LOG_FULL);
527
0
    if (chunk < 0)
528
0
    {
529
0
      adata->status = POP_DISCONNECTED;
530
0
      rc = -1;
531
0
      break;
532
0
    }
533
534
0
    char *p = buf;
535
0
    if (!lenbuf && (buf[0] == '.'))
536
0
    {
537
0
      if (buf[1] != '.')
538
0
        break;
539
0
      p++;
540
0
    }
541
542
0
    mutt_str_copy(inbuf + lenbuf, p, sizeof(buf));
543
0
    pos += chunk;
544
545
    /* cast is safe since we break out of the loop when chunk<=0 */
546
0
    if ((size_t) chunk >= sizeof(buf))
547
0
    {
548
0
      lenbuf += strlen(p);
549
0
    }
550
0
    else
551
0
    {
552
0
      if (progress)
553
0
        progress_update(progress, pos, -1);
554
0
      if ((rc == 0) && (callback(inbuf, data) < 0))
555
0
        rc = -3;
556
0
      lenbuf = 0;
557
0
    }
558
559
0
    mutt_mem_realloc(&inbuf, lenbuf + sizeof(buf));
560
0
  }
561
562
0
  FREE(&inbuf);
563
0
  return rc;
564
0
}
565
566
/**
567
 * check_uidl - Find message with this UIDL and set refno - Implements ::pop_fetch_t - @ingroup pop_fetch_api
568
 * @param line String containing UIDL
569
 * @param data POP data
570
 * @retval  0 Success
571
 * @retval -1 Error
572
 */
573
static int check_uidl(const char *line, void *data)
574
0
{
575
0
  if (!line || !data)
576
0
    return -1;
577
578
0
  char *endp = NULL;
579
580
0
  errno = 0;
581
0
  unsigned int index = strtoul(line, &endp, 10);
582
0
  if (errno != 0)
583
0
    return -1;
584
0
  while (*endp == ' ')
585
0
    endp++;
586
587
0
  struct Mailbox *m = data;
588
0
  for (int i = 0; i < m->msg_count; i++)
589
0
  {
590
0
    struct PopEmailData *edata = pop_edata_get(m->emails[i]);
591
0
    if (mutt_str_equal(edata->uid, endp))
592
0
    {
593
0
      edata->refno = index;
594
0
      break;
595
0
    }
596
0
  }
597
598
0
  return 0;
599
0
}
600
601
/**
602
 * pop_reconnect - Reconnect and verify indexes if connection was lost
603
 * @param m Mailbox
604
 * @retval  0 Success
605
 * @retval -1 Error
606
 */
607
int pop_reconnect(struct Mailbox *m)
608
0
{
609
0
  struct PopAccountData *adata = pop_adata_get(m);
610
611
0
  if (adata->status == POP_CONNECTED)
612
0
    return 0;
613
614
0
  while (true)
615
0
  {
616
0
    mutt_socket_close(adata->conn);
617
618
0
    int rc = pop_open_connection(adata);
619
0
    if (rc == 0)
620
0
    {
621
0
      struct Progress *progress = progress_new(_("Verifying message indexes..."),
622
0
                                               MUTT_PROGRESS_NET, 0);
623
624
0
      for (int i = 0; i < m->msg_count; i++)
625
0
      {
626
0
        struct PopEmailData *edata = pop_edata_get(m->emails[i]);
627
0
        edata->refno = -1;
628
0
      }
629
630
0
      rc = pop_fetch_data(adata, "UIDL\r\n", progress, check_uidl, m);
631
0
      progress_free(&progress);
632
0
      if (rc == -2)
633
0
      {
634
0
        mutt_error("%s", adata->err_msg);
635
0
      }
636
0
    }
637
638
0
    if (rc == 0)
639
0
      return 0;
640
641
0
    pop_logout(m);
642
643
0
    if (rc < -1)
644
0
      return -1;
645
646
0
    const enum QuadOption c_pop_reconnect = cs_subset_quad(NeoMutt->sub, "pop_reconnect");
647
0
    if (query_quadoption(c_pop_reconnect, _("Connection lost. Reconnect to POP server?")) != MUTT_YES)
648
0
    {
649
0
      return -1;
650
0
    }
651
0
  }
652
0
}