Check-in [2b96941c4c]
Not logged in

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Automatic retry on an SMTP relay failure.
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 2b96941c4c924e56246d673a30d5ef5146824dfd7361af503c55bbf66fc4eac8
User & Date: drh 2025-04-16 00:48:50.543
Context
2025-04-16
00:58
Break out SMTP faults as a separate category on the Error Log. check-in: 2d3ace5a9f user: drh tags: trunk
00:48
Automatic retry on an SMTP relay failure. check-in: 2b96941c4c user: drh tags: trunk
2025-04-15
23:34
Attempt to provide improved error message outputs for failures while trying to send notification via relay to an MTA. check-in: e6c27d3dab user: drh tags: trunk
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/alerts.c.
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
                                  smtpFlags, 0);
      if( p->pSmtp==0 || p->pSmtp->zErr ){
        emailerError(p, "Could not start SMTP session: %s",
                        p->pSmtp ? p->pSmtp->zErr : "reason unknown");
      }else if( p->zDest[0]=='d' ){
        smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
      }
      smtp_client_startup(p->pSmtp);
    }
  }
  return p;
}

/*
** Scan the header of the email message in pMsg looking for the







<







653
654
655
656
657
658
659

660
661
662
663
664
665
666
                                  smtpFlags, 0);
      if( p->pSmtp==0 || p->pSmtp->zErr ){
        emailerError(p, "Could not start SMTP session: %s",
                        p->pSmtp ? p->pSmtp->zErr : "reason unknown");
      }else if( p->zDest[0]=='d' ){
        smtp_session_config(p->pSmtp, SMTP_TRACE_BLOB, &p->out);
      }

    }
  }
  return p;
}

/*
** Scan the header of the email message in pMsg looking for the
Changes to src/smtp.c.
154
155
156
157
158
159
160
161
162
163
164

165
166
167
168
169
170
171
struct SmtpSession {
  const char *zFrom;        /* Domain from which we are sending */
  const char *zDest;        /* Domain that will receive the email */
  char *zHostname;          /* Hostname of SMTP server for zDest */
  u32 smtpFlags;            /* Flags changing the operation */
  FILE *logFile;            /* Write session transcript to this log file */
  Blob *pTranscript;        /* Record session transcript here */
  int atEof;                /* True after connection closes */
  int bFatal;               /* Error is fatal.  Do not retry */
  char *zErr;               /* Error message */
  Blob inbuf;               /* Input buffer */

};

/* Allowed values for SmtpSession.smtpFlags */
#define SMTP_TRACE_STDOUT   0x00001     /* Debugging info to console */
#define SMTP_TRACE_FILE     0x00002     /* Debugging info to logFile */
#define SMTP_TRACE_BLOB     0x00004     /* Record transcript */
#define SMTP_DIRECT         0x00008     /* Skip the MX lookup */







|



>







154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
struct SmtpSession {
  const char *zFrom;        /* Domain from which we are sending */
  const char *zDest;        /* Domain that will receive the email */
  char *zHostname;          /* Hostname of SMTP server for zDest */
  u32 smtpFlags;            /* Flags changing the operation */
  FILE *logFile;            /* Write session transcript to this log file */
  Blob *pTranscript;        /* Record session transcript here */
  int bOpen;                /* True if connection is Open */
  int bFatal;               /* Error is fatal.  Do not retry */
  char *zErr;               /* Error message */
  Blob inbuf;               /* Input buffer */
  UrlData url;              /* Address of the server */
};

/* Allowed values for SmtpSession.smtpFlags */
#define SMTP_TRACE_STDOUT   0x00001     /* Debugging info to console */
#define SMTP_TRACE_FILE     0x00002     /* Debugging info to logFile */
#define SMTP_TRACE_BLOB     0x00004     /* Record transcript */
#define SMTP_DIRECT         0x00008     /* Skip the MX lookup */
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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
  if( bFatal ) p->bFatal = 1;
  if( p->zErr==0 ){
    va_list ap;
    va_start(ap, zFormat);
    p->zErr = vmprintf(zFormat, ap);
    va_end(ap);
  }

  socket_close();
  p->atEof = 1;

}

/*
** Allocate a new SmtpSession object.
**
** Both zFrom and zDest must be specified.  smtpFlags may not contain
** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
** added by a subsequent call to smtp_session_config().
**
** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
*/
SmtpSession *smtp_session_new(
  const char *zFrom,    /* Domain for the client */
  const char *zDest,    /* Domain of the server */
  u32 smtpFlags,        /* Flags */
  int iPort             /* TCP port if the SMTP_PORT flags is present */
){
  SmtpSession *p;
  UrlData url;

  p = fossil_malloc( sizeof(*p) );
  memset(p, 0, sizeof(*p));
  p->zFrom = zFrom;
  p->zDest = zDest;
  p->smtpFlags = smtpFlags;
  memset(&url, 0, sizeof(url));
  url.port = 25;
  blob_init(&p->inbuf, 0, 0);
  if( smtpFlags & SMTP_PORT ){
    url.port = iPort;
  }
  if( (smtpFlags & SMTP_DIRECT)!=0 ){
    int i;
    p->zHostname = fossil_strdup(zDest);
    for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
    if( p->zHostname[i]==':' ){
      p->zHostname[i] = 0;
      url.port = atoi(&p->zHostname[i+1]);
    }
  }else{
    p->zHostname = smtp_mx_host(zDest);
  }
  if( p->zHostname==0 ){
    smtp_set_error(p, 1, "cannot locate SMTP server for \"%s\"", zDest);
    return p;
  }
  url.name = p->zHostname;
  socket_global_init();
  if( socket_open(&url) ){
    smtp_set_error(p, 1, "can't open socket: %z", socket_errmsg());
  }
  return p;
}

/*
** Configure debugging options on SmtpSession.  Add all bits in
** smtpFlags to the settings.  The following bits can be added:
**







>
|
|
>


















<






<
|


|







|








|

|
<
<







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
222
223
224
225

226
227
228
229
230
231

232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254


255
256
257
258
259
260
261
  if( bFatal ) p->bFatal = 1;
  if( p->zErr==0 ){
    va_list ap;
    va_start(ap, zFormat);
    p->zErr = vmprintf(zFormat, ap);
    va_end(ap);
  }
  if( p->bOpen ){
    socket_close();
    p->bOpen = 0;
  }
}

/*
** Allocate a new SmtpSession object.
**
** Both zFrom and zDest must be specified.  smtpFlags may not contain
** either SMTP_TRACE_FILE or SMTP_TRACE_BLOB as those settings must be
** added by a subsequent call to smtp_session_config().
**
** The iPort option is ignored unless SMTP_PORT is set in smtpFlags
*/
SmtpSession *smtp_session_new(
  const char *zFrom,    /* Domain for the client */
  const char *zDest,    /* Domain of the server */
  u32 smtpFlags,        /* Flags */
  int iPort             /* TCP port if the SMTP_PORT flags is present */
){
  SmtpSession *p;


  p = fossil_malloc( sizeof(*p) );
  memset(p, 0, sizeof(*p));
  p->zFrom = zFrom;
  p->zDest = zDest;
  p->smtpFlags = smtpFlags;

  p->url.port = 25;
  blob_init(&p->inbuf, 0, 0);
  if( smtpFlags & SMTP_PORT ){
    p->url.port = iPort;
  }
  if( (smtpFlags & SMTP_DIRECT)!=0 ){
    int i;
    p->zHostname = fossil_strdup(zDest);
    for(i=0; p->zHostname[i] && p->zHostname[i]!=':'; i++){}
    if( p->zHostname[i]==':' ){
      p->zHostname[i] = 0;
      p->url.port = atoi(&p->zHostname[i+1]);
    }
  }else{
    p->zHostname = smtp_mx_host(zDest);
  }
  if( p->zHostname==0 ){
    smtp_set_error(p, 1, "cannot locate SMTP server for \"%s\"", zDest);
    return p;
  }
  p->url.name = p->zHostname;
  socket_global_init();
  p->bOpen = 0;


  return p;
}

/*
** Configure debugging options on SmtpSession.  Add all bits in
** smtpFlags to the settings.  The following bits can be added:
**
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
** Send a single line of output the SMTP client to the server.
*/
static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
  Blob b = empty_blob;
  va_list ap;
  char *z;
  int n;
  if( p->atEof ) return;
  va_start(ap, zFormat);
  blob_vappendf(&b, zFormat, ap);
  va_end(ap);
  z = blob_buffer(&b);
  n = blob_size(&b);
  assert( n>=2 );
  assert( z[n-1]=='\n' );







|







276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
** Send a single line of output the SMTP client to the server.
*/
static void smtp_send_line(SmtpSession *p, const char *zFormat, ...){
  Blob b = empty_blob;
  va_list ap;
  char *z;
  int n;
  if( !p->bOpen ) return;
  va_start(ap, zFormat);
  blob_vappendf(&b, zFormat, ap);
  va_end(ap);
  z = blob_buffer(&b);
  n = blob_size(&b);
  assert( n>=2 );
  assert( z[n-1]=='\n' );
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
static void smtp_recv_line(SmtpSession *p, Blob *in){
  int n = blob_size(&p->inbuf);
  char *z = blob_buffer(&p->inbuf);
  int i = blob_tell(&p->inbuf);
  int nDelay = 0;
  if( i<n && z[n-1]=='\n' ){
    blob_line(&p->inbuf, in);
  }else if( p->atEof ){
    blob_init(in, 0, 0);
  }else{
    if( n>0 && i>=n ){
      blob_truncate(&p->inbuf, 0);
      blob_rewind(&p->inbuf);
      n = 0;
    }







|







312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
static void smtp_recv_line(SmtpSession *p, Blob *in){
  int n = blob_size(&p->inbuf);
  char *z = blob_buffer(&p->inbuf);
  int i = blob_tell(&p->inbuf);
  int nDelay = 0;
  if( i<n && z[n-1]=='\n' ){
    blob_line(&p->inbuf, in);
  }else if( !p->bOpen ){
    blob_init(in, 0, 0);
  }else{
    if( n>0 && i>=n ){
      blob_truncate(&p->inbuf, 0);
      blob_rewind(&p->inbuf);
      n = 0;
    }
374
375
376
377
378
379
380

381
382
383
384
385
386
387
  int *pbMore,      /* True if the reply is not complete */
  char **pzArg      /* Argument */
){
  int n;
  char *z;
  blob_truncate(in, 0);
  smtp_recv_line(p, in);

  z = blob_str(in);
  n = blob_size(in);
  if( z[0]=='#' ){
    *piCode = 0;
    *pbMore = 1;
    *pzArg = z;
  }else{







>







373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
  int *pbMore,      /* True if the reply is not complete */
  char **pzArg      /* Argument */
){
  int n;
  char *z;
  blob_truncate(in, 0);
  smtp_recv_line(p, in);
  blob_trim(in);
  z = blob_str(in);
  n = blob_size(in);
  if( z[0]=='#' ){
    *piCode = 0;
    *pbMore = 1;
    *pzArg = z;
  }else{
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409

410
411
412
413
414
415
416
417
418
419
420
421
422
423
424


425

426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443


444
445
446
447
448
449
450
** Have the client send a QUIT message.
*/
int smtp_client_quit(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  if( !p->atEof ){
    smtp_send_line(p, "QUIT\r\n");
    do{
      smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
    }while( bMore );
    p->atEof = 1;
  }
  socket_close();

  return 0;
}

/*
** Begin a client SMTP session.  Wait for the initial 220 then send
** the EHLO and wait for a 250.
**
** Return 0 on success and non-zero for a failure.
*/
int smtp_client_startup(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  if( p==0 || p->bFatal ) return 1;


  fossil_free(p->zErr);

  p->zErr = 0;
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=220 ){
    smtp_set_error(p, 1, "server opens conversation with: %b", &in);
    smtp_client_quit(p);
    return 1;
  }
  smtp_send_line(p, "EHLO %s\r\n", p->zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 1, "server responds to EHLO with: %b", &in);
    smtp_client_quit(p);
    return 1;
  }


  return 0;
}

/*
** COMMAND: test-smtp-probe
**
** Usage: %fossil test-smtp-probe DOMAIN [ME]







|




|
<
|
>









|





>
>
|
>
|




|








|



>
>







395
396
397
398
399
400
401
402
403
404
405
406
407

408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
** Have the client send a QUIT message.
*/
int smtp_client_quit(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  if( p->bOpen ){
    smtp_send_line(p, "QUIT\r\n");
    do{
      smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
    }while( bMore );
    p->bOpen = 0;

    socket_close();
  }
  return 0;
}

/*
** Begin a client SMTP session.  Wait for the initial 220 then send
** the EHLO and wait for a 250.
**
** Return 0 on success and non-zero for a failure.
*/
static int smtp_client_startup(SmtpSession *p){
  Blob in = BLOB_INITIALIZER;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  if( p==0 || p->bFatal ) return 1;
  if( socket_open(&p->url) ){
    smtp_set_error(p, 1, "can't open socket: %z", socket_errmsg());
    return 1;
  }
  p->bOpen = 1;
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=220 ){
    smtp_set_error(p, 1, "conversation begins with: \"%d %s\"",iCode,zArg);
    smtp_client_quit(p);
    return 1;
  }
  smtp_send_line(p, "EHLO %s\r\n", p->zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 1, "reply to EHLO with: \"%d %s\"",iCode, zArg);
    smtp_client_quit(p);
    return 1;
  }
  fossil_free(p->zErr);
  p->zErr = 0;
  return 0;
}

/*
** COMMAND: test-smtp-probe
**
** Usage: %fossil test-smtp-probe DOMAIN [ME]
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619

620
621
622
623
624
625
626
){
  int i;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  Blob in;
  blob_init(&in, 0, 0);
  if( p->atEof && !p->bFatal ){
    smtp_client_startup(p);
    if( p->atEof ) return 1;
  }
  smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 0, "server replies to MAIL FROM with: %b", &in);
    return 1;
  }
  for(i=0; i<nTo; i++){
    smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]);
    do{
      smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
    }while( bMore );
    if( iCode!=250 ){
      smtp_set_error(p, 0, "server replies to RCPT TO with: %b", &in);
      return 1;
    }
  }
  smtp_send_line(p, "DATA\r\n");
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=354 ){
    smtp_set_error(p, 0, "server replies to DATA with: %b", &in);
    return 1;
  }
  smtp_send_email_body(zMsg, socket_send, 0);
  if( p->smtpFlags & SMTP_TRACE_STDOUT ){
    fossil_print("C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_FILE ){
    fprintf(p->logFile, "C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_BLOB ){
    blob_appendf(p->pTranscript, "C: # message content\nC: .\n");
  }
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 0, "server replies to end-of-DATA with: %b", &in);

    return 1;
  }
  return 0;
}

/*
** The input is a base email address of the form "local@domain".







|
|
|






|








|








|
















|
>







573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
){
  int i;
  int iCode = 0;
  int bMore = 0;
  char *zArg = 0;
  Blob in;
  blob_init(&in, 0, 0);
  if( !p->bOpen ){
    if( !p->bFatal ) smtp_client_startup(p);
    if( !p->bOpen ) return 1;
  }
  smtp_send_line(p, "MAIL FROM:<%s>\r\n", zFrom);
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 0,"reply to MAIL FROM: \"%d %s\"",iCode,zArg);
    return 1;
  }
  for(i=0; i<nTo; i++){
    smtp_send_line(p, "RCPT TO:<%s>\r\n", azTo[i]);
    do{
      smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
    }while( bMore );
    if( iCode!=250 ){
      smtp_set_error(p, 0,"reply to RCPT TO: \"%d %s\"",iCode,zArg);
      return 1;
    }
  }
  smtp_send_line(p, "DATA\r\n");
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=354 ){
    smtp_set_error(p, 0, "reply to DATA with: \"%d %s\"",iCode,zArg);
    return 1;
  }
  smtp_send_email_body(zMsg, socket_send, 0);
  if( p->smtpFlags & SMTP_TRACE_STDOUT ){
    fossil_print("C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_FILE ){
    fprintf(p->logFile, "C: # message content\nC: .\n");
  }
  if( p->smtpFlags & SMTP_TRACE_BLOB ){
    blob_appendf(p->pTranscript, "C: # message content\nC: .\n");
  }
  do{
    smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg);
  }while( bMore );
  if( iCode!=250 ){
    smtp_set_error(p, 0, "reply to end-of-DATA with: \"%d %s\"",
                   iCode, zArg);
    return 1;
  }
  return 0;
}

/*
** The input is a base email address of the form "local@domain".
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
    zToDomain = domain_of_addr(azTo[0]);
  }
  p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort);
  if( p->zErr ){
    fossil_fatal("%s", p->zErr);
  }
  fossil_print("Connection to \"%s\"\n", p->zHostname);
  smtp_client_startup(p);
  if( !p->atEof ){
    smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body));
  }
  smtp_client_quit(p);
  if( p->zErr ){
    fossil_fatal("ERROR: %s\n", p->zErr);
  }
  smtp_session_free(p);
  blob_reset(&body);
}







<
<
|
<







685
686
687
688
689
690
691


692

693
694
695
696
697
698
699
    zToDomain = domain_of_addr(azTo[0]);
  }
  p = smtp_session_new(zFromDomain, zToDomain, smtpFlags, smtpPort);
  if( p->zErr ){
    fossil_fatal("%s", p->zErr);
  }
  fossil_print("Connection to \"%s\"\n", p->zHostname);


  smtp_send_msg(p, zFrom, nTo, azTo, blob_str(&body));

  smtp_client_quit(p);
  if( p->zErr ){
    fossil_fatal("ERROR: %s\n", p->zErr);
  }
  smtp_session_free(p);
  blob_reset(&body);
}