Index: auto.def ================================================================== --- auto.def +++ auto.def @@ -478,10 +478,11 @@ # Last resort, may be Windows if {[is_mingw]} { define-append LIBS -lwsock32 } } +cc-check-function-in-lib ns_name_uncompress resolv cc-check-functions utime cc-check-functions usleep cc-check-functions strchrnul cc-check-functions pledge Index: src/email.c ================================================================== --- src/email.c +++ src/email.c @@ -13,11 +13,11 @@ ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** -** Email notification features +** Logic for email notification, also known as "alerts". */ #include "config.h" #include "email.h" #include #include @@ -181,21 +181,22 @@ } } /* -** WEBPAGE: setup_email +** WEBPAGE: setup_notification ** ** Administrative page for configuring and controlling email notification. -** Normally accessible via the /Admin/Email menu. +** Normally accessible via the /Admin/Notification menu. */ -void setup_email(void){ +void setup_notification(void){ static const char *const azSendMethods[] = { - "off", "Disabled", - "pipe", "Pipe to a command", - "db", "Store in a database", - "dir", "Store in a directory" + "off", "Disabled", + "pipe", "Pipe to a command", + "db", "Store in a database", + "dir", "Store in a directory", + "relay", "SMTP relay" }; login_check_credentials(); if( !g.perm.Setup ){ login_needed(0); return; @@ -202,11 +203,11 @@ } db_begin_transaction(); email_submenu_common(); style_header("Email Notification Setup"); - @
+ @
@
login_insert_csrf_secret(); entry_attribute("Canonical Server URL", 40, "email-url", "eurl", "", 0); @@ -244,17 +245,14 @@ @ (Property: "email-autoexec")

@
multiple_choice_attribute("Email Send Method", "email-send-method", "esm", "off", count(azSendMethods)/2, azSendMethods); - @

How to send email. The "Pipe to a command" - @ method is the usual choice in production. - @ (Property: "email-send-method")

- @
+ @

How to send email. Requires auxiliary information from the fields + @ that follow. (Property: "email-send-method")

email_schema(1); - - entry_attribute("Command To Pipe Email To", 80, "email-send-command", + entry_attribute("Command To Pipe Email To", 60, "email-send-command", "ecmd", "sendmail -t", 0); @

When the send method is "pipe to a command", this is the command @ that is run. Email messages are piped into the standard input of this @ command. The command is expected to extract the sender address, @ recepient addresses, and subject from the header of the piped email @@ -269,10 +267,19 @@ entry_attribute("Directory In Which To Store Email", 60, "email-send-dir", "esdir", "", 0); @

When the send method is "store in a directory", each email message is @ stored as a separate file in the directory shown here. @ (Property: "email-send-dir")

+ + entry_attribute("SMTP relay host", 60, "email-send-relayhost", + "esrh", "", 0); + @

When the send method is "SMTP relay", each email message is + @ transmitted via the SMTP protocol (rfc5321) to a "Mail Submission + @ Agent" or "MSA" (rfc4409) at the hostname shown here. Optionally + @ append a colon and TCP port number (ex: smtp.example.com:587). + @ The default TCP port number is 25. + @ (Property: "email-send-relayhost")

@
entry_attribute("Administrator email address", 40, "email-admin", "eadmin", "", 0); @

This is the email for the human administrator for the system. @@ -376,14 +383,21 @@ const char *zDest; /* How to send email. */ const char *zDb; /* Name of database file */ const char *zDir; /* Directory in which to store as email files */ const char *zCmd; /* Command to run for each email */ const char *zFrom; /* Emails come from here */ + SmtpSession *pSmtp; /* SMTP relay connection */ Blob out; /* For zDest=="blob" */ char *zErr; /* Error message */ + u32 mFlags; /* Flags */ int bImmediateFail; /* On any error, call fossil_fatal() */ }; + +/* Allowed values for EmailSender flags */ +#define EMAIL_IMMEDIATE_FAIL 0x0001 /* Call fossil_fatal() on any error */ +#define EMAIL_SMTP_TRACE 0x0002 /* Write SMTP transcript to console */ + #endif /* INTERFACE */ /* ** Shutdown an emailer. Clear all information other than the error message. */ @@ -393,12 +407,16 @@ sqlite3_close(p->db); p->db = 0; p->zDb = 0; p->zDir = 0; p->zCmd = 0; - p->zDest = "off"; - blob_zero(&p->out); + if( p->pSmtp ){ + smtp_client_quit(p->pSmtp); + smtp_session_free(p->pSmtp); + p->pSmtp = 0; + } + blob_reset(&p->out); } /* ** Put the EmailSender into an error state. */ @@ -407,11 +425,11 @@ fossil_free(p->zErr); va_start(ap, zFormat); p->zErr = vmprintf(zFormat, ap); va_end(ap); emailerShutdown(p); - if( p->bImmediateFail ){ + if( p->mFlags & EMAIL_IMMEDIATE_FAIL ){ fossil_fatal("%s", p->zErr); } } /* @@ -455,16 +473,17 @@ ** zAltDest to cause all emails to be printed to the console for ** debugging purposes. ** ** The EmailSender object returned must be freed using email_sender_free(). */ -EmailSender *email_sender_new(const char *zAltDest, int bImmediateFail){ +EmailSender *email_sender_new(const char *zAltDest, u32 mFlags){ EmailSender *p; p = fossil_malloc(sizeof(*p)); memset(p, 0, sizeof(*p)); - p->bImmediateFail = bImmediateFail; + blob_init(&p->out, 0, 0); + p->mFlags = mFlags; if( zAltDest ){ p->zDest = zAltDest; }else{ p->zDest = db_get("email-send-method","off"); } @@ -499,13 +518,149 @@ emailerGetSetting(p, &p->zCmd, "email-send-command"); }else if( fossil_strcmp(p->zDest, "dir")==0 ){ emailerGetSetting(p, &p->zDir, "email-send-dir"); }else if( fossil_strcmp(p->zDest, "blob")==0 ){ blob_init(&p->out, 0, 0); + }else if( fossil_strcmp(p->zDest, "relay")==0 ){ + const char *zRelay = 0; + emailerGetSetting(p, &zRelay, "email-send-relayhost"); + if( zRelay ){ + u32 smtpFlags = SMTP_DIRECT; + if( mFlags & EMAIL_SMTP_TRACE ) smtpFlags |= SMTP_TRACE_STDOUT; + p->pSmtp = smtp_session_new(p->zFrom, zRelay, smtpFlags); + smtp_client_startup(p->pSmtp); + } } return p; } + +/* +** Scan the header of the email message in pMsg looking for the +** (first) occurrance of zField. Fill pValue with the content of +** that field. +** +** This routine initializes pValue. Any prior content of pValue is +** discarded (leaked). +** +** Return non-zero on success. Return 0 if no instance of the header +** is found. +*/ +int email_header_value(Blob *pMsg, const char *zField, Blob *pValue){ + int nField = (int)strlen(zField); + Blob line; + blob_rewind(pMsg); + blob_init(pValue,0,0); + while( blob_line(pMsg, &line) ){ + int n, i; + char *z; + blob_trim(&line); + n = blob_size(&line); + if( n==0 ) return 0; + if( n" character. +** +** Verify that the string really that is to be copied really is a +** valid email address. If it is not, then return NULL. +** +** This routine is more restrictive than necessary. It does not +** allow comments, IP address, quoted strings, or certain uncommon +** characters. The only non-alphanumerics allowed in the local +** part are "_", "+", "-" and "+". +*/ +char *email_copy_addr(const char *z){ + int i; + int nAt = 0; + int nDot = 0; + char c; + if( z[0]=='.' ) return 0; /* Local part cannot begin with "." */ + for(i=0; (c = z[i])!=0 && c!='>'; i++){ + if( fossil_isalnum(c) ){ + /* Alphanumerics are always ok */ + }else if( c=='@' ){ + if( nAt ) return 0; /* Only a single "@" allowed */ + if( i>64 ) return 0; /* Local part too big */ + nAt = 1; + nDot = 0; + if( i==0 ) return 0; /* Disallow empty local part */ + if( z[i-1]=='.' ) return 0; /* Last char of local cannot be "." */ + if( z[i+1]=='.' || z[i+1]=='-' ){ + return 0; /* Domain cannot begin with "." or "-" */ + } + }else if( c=='-' ){ + if( z[i+1]=='>' ) return 0; /* Last character cannot be "-" */ + }else if( c=='.' ){ + if( z[i+1]=='.' ) return 0; /* Do not allow ".." */ + if( z[i+1]=='>' ) return 0; /* Domain may not end with . */ + nDot++; + }else if( (c=='_' || c=='+') && nAt==0 ){ + /* _ and + are ok in the local part */ + }else{ + return 0; /* Anything else is an error */ + } + } + if( c!='>' ) return 0; /* Missing final ">" */ + if( nAt==0 ) return 0; /* No "@" found anywhere */ + if( nDot==0 ) return 0; /* No "." in the domain */ + + /* If we reach this point, the email address is valid */ + return mprintf("%.*s", i, z); +} + +/* +** Extract all To: header values from the email header supplied. +** Store them in the array list. +*/ +void email_header_to(Blob *pMsg, int *pnTo, char ***pazTo){ + int nTo = 0; + char **azTo = 0; + Blob v; + char *z, *zAddr; + int i; + + email_header_value(pMsg, "to", &v); + z = blob_str(&v); + for(i=0; z[i]; i++){ + if( z[i]=='<' && (zAddr = email_copy_addr(&z[i+1]))!=0 ){ + azTo = fossil_realloc(azTo, sizeof(azTo[0])*(nTo+1) ); + azTo[nTo++] = zAddr; + } + } + *pnTo = nTo; + *pazTo = azTo; +} + +/* +** Free a list of To addresses obtained from a prior call to +** email_header_to() +*/ +void email_header_to_free(int nTo, char **azTo){ + int i; + for(i=0; izDir ){ char *zFile = emailTempFilename(p->zDir); blob_write_to_file(&all, zFile); fossil_free(zFile); + }else if( p->pSmtp ){ + char **azTo = 0; + int nTo = 0; + email_header_to(pHdr, &nTo, &azTo); + if( nTo>0 ){ + smtp_send_msg(p->pSmtp, p->zFrom, nTo, (const char**)azTo,blob_str(&all)); + email_header_to_free(nTo, azTo); + } }else if( strcmp(p->zDest, "stdout")==0 ){ + char **azTo = 0; + int nTo = 0; + int i; + email_header_to(pHdr, &nTo, &azTo); + for(i=0; i\n", zEAddr); blob_appendf(&hdr, "Subject: Subscription verification\n"); blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/, g.zBaseURL, g.zBaseURL, zCode); email_send(pSender, &hdr, &body); style_header("Email Alert Verification"); @@ -1582,11 +1763,11 @@ ** Free a linked list of EmailEvent objects */ void email_free_eventlist(EmailEvent *p){ while( p ){ EmailEvent *pNext = p->pNext; - blob_zero(&p->txt); + blob_reset(&p->txt); fossil_free(p); p = pNext; } } @@ -1715,11 +1896,11 @@ blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt)); } email_free_eventlist(pEvent); email_footer(&out); fossil_print("%s", blob_str(&out)); - blob_zero(&out); + blob_reset(&out); db_end_transaction(0); } /* ** COMMAND: test-add-alerts @@ -1837,12 +2018,12 @@ zUrl, zCode); email_send(pSender,&hdr,&body); blob_truncate(&hdr, 0); blob_truncate(&body, 0); } - blob_zero(&hdr); - blob_zero(&body); + blob_reset(&hdr); + blob_reset(&body); db_finalize(&q); email_free_eventlist(pEvents); if( (flags & SENDALERT_PRESERVE)==0 ){ if( flags & SENDALERT_DIGEST ){ db_multi_exec("UPDATE pending_alert SET sentDigest=true"); Index: src/http_socket.c ================================================================== --- src/http_socket.c +++ src/http_socket.c @@ -79,12 +79,14 @@ } /* ** Return the current socket error message */ -const char *socket_errmsg(void){ - return socketErrMsg; +char *socket_errmsg(void){ + char *zResult = socketErrMsg; + socketErrMsg = 0; + return zResult; } /* ** Call this routine once before any other use of the socket interface. ** This routine does initial configuration of the socket module. @@ -132,12 +134,12 @@ /* ** Open a socket connection. The identify of the server is determined ** by pUrlData ** -** pUrlDAta->name Name of the server. Ex: www.fossil-scm.org -** pUrlDAta->port TCP/IP port to use. Ex: 80 +** pUrlData->name Name of the server. Ex: www.fossil-scm.org +** pUrlData->port TCP/IP port to use. Ex: 80 ** ** Return the number of errors. */ int socket_open(UrlData *pUrlData){ int rc = 0; @@ -190,11 +192,11 @@ } /* ** Send content out over the open socket connection. */ -size_t socket_send(void *NotUsed, void *pContent, size_t N){ +size_t socket_send(void *NotUsed, const void *pContent, size_t N){ size_t sent; size_t total = 0; while( N>0 ){ sent = send(iSocket, pContent, N, 0); if( sent<=0 ) break; @@ -205,17 +207,25 @@ return total; } /* ** Receive content back from the open socket connection. +** Return the number of bytes read. +** +** When bDontBlock is false, this function blocks until all N bytes +** have been read. */ -size_t socket_receive(void *NotUsed, void *pContent, size_t N){ +size_t socket_receive(void *NotUsed, void *pContent, size_t N, int bDontBlock){ ssize_t got; size_t total = 0; + int flags = 0; +#ifdef MSG_DONTWAIT + if( bDontBlock ) flags |= MSG_DONTWAIT; +#endif while( N>0 ){ /* WinXP fails for large values of N. So limit it to 64KiB. */ - got = recv(iSocket, pContent, N>65536 ? 65536 : N, 0); + got = recv(iSocket, pContent, N>65536 ? 65536 : N, flags); if( got<=0 ) break; total += (size_t)got; N -= (size_t)got; pContent = (void*)&((char*)pContent)[got]; } Index: src/http_transport.c ================================================================== --- src/http_transport.c +++ src/http_transport.c @@ -325,11 +325,11 @@ got = 0; #endif }else if( pUrlData->isFile ){ got = fread(zBuf, 1, N, transport.pFile); }else{ - got = socket_receive(0, zBuf, N); + got = socket_receive(0, zBuf, N, 0); } /* printf("received %d of %d bytes\n", got, N); fflush(stdout); */ if( transport.pLog ){ fwrite(zBuf, 1, got, transport.pLog); fflush(transport.pLog); Index: src/main.mk ================================================================== --- src/main.mk +++ src/main.mk @@ -113,10 +113,11 @@ $(SRCDIR)/sha1hard.c \ $(SRCDIR)/sha3.c \ $(SRCDIR)/shun.c \ $(SRCDIR)/sitemap.c \ $(SRCDIR)/skins.c \ + $(SRCDIR)/smtp.c \ $(SRCDIR)/sqlcmd.c \ $(SRCDIR)/stash.c \ $(SRCDIR)/stat.c \ $(SRCDIR)/statrep.c \ $(SRCDIR)/style.c \ @@ -135,10 +136,11 @@ $(SRCDIR)/user.c \ $(SRCDIR)/utf8.c \ $(SRCDIR)/util.c \ $(SRCDIR)/verify.c \ $(SRCDIR)/vfile.c \ + $(SRCDIR)/webmail.c \ $(SRCDIR)/wiki.c \ $(SRCDIR)/wikiformat.c \ $(SRCDIR)/winfile.c \ $(SRCDIR)/winhttp.c \ $(SRCDIR)/wysiwyg.c \ @@ -316,10 +318,11 @@ $(OBJDIR)/sha1hard_.c \ $(OBJDIR)/sha3_.c \ $(OBJDIR)/shun_.c \ $(OBJDIR)/sitemap_.c \ $(OBJDIR)/skins_.c \ + $(OBJDIR)/smtp_.c \ $(OBJDIR)/sqlcmd_.c \ $(OBJDIR)/stash_.c \ $(OBJDIR)/stat_.c \ $(OBJDIR)/statrep_.c \ $(OBJDIR)/style_.c \ @@ -338,10 +341,11 @@ $(OBJDIR)/user_.c \ $(OBJDIR)/utf8_.c \ $(OBJDIR)/util_.c \ $(OBJDIR)/verify_.c \ $(OBJDIR)/vfile_.c \ + $(OBJDIR)/webmail_.c \ $(OBJDIR)/wiki_.c \ $(OBJDIR)/wikiformat_.c \ $(OBJDIR)/winfile_.c \ $(OBJDIR)/winhttp_.c \ $(OBJDIR)/wysiwyg_.c \ @@ -448,10 +452,11 @@ $(OBJDIR)/sha1hard.o \ $(OBJDIR)/sha3.o \ $(OBJDIR)/shun.o \ $(OBJDIR)/sitemap.o \ $(OBJDIR)/skins.o \ + $(OBJDIR)/smtp.o \ $(OBJDIR)/sqlcmd.o \ $(OBJDIR)/stash.o \ $(OBJDIR)/stat.o \ $(OBJDIR)/statrep.o \ $(OBJDIR)/style.o \ @@ -470,10 +475,11 @@ $(OBJDIR)/user.o \ $(OBJDIR)/utf8.o \ $(OBJDIR)/util.o \ $(OBJDIR)/verify.o \ $(OBJDIR)/vfile.o \ + $(OBJDIR)/webmail.o \ $(OBJDIR)/wiki.o \ $(OBJDIR)/wikiformat.o \ $(OBJDIR)/winfile.o \ $(OBJDIR)/winhttp.o \ $(OBJDIR)/wysiwyg.o \ @@ -778,10 +784,11 @@ $(OBJDIR)/sha1hard_.c:$(OBJDIR)/sha1hard.h \ $(OBJDIR)/sha3_.c:$(OBJDIR)/sha3.h \ $(OBJDIR)/shun_.c:$(OBJDIR)/shun.h \ $(OBJDIR)/sitemap_.c:$(OBJDIR)/sitemap.h \ $(OBJDIR)/skins_.c:$(OBJDIR)/skins.h \ + $(OBJDIR)/smtp_.c:$(OBJDIR)/smtp.h \ $(OBJDIR)/sqlcmd_.c:$(OBJDIR)/sqlcmd.h \ $(OBJDIR)/stash_.c:$(OBJDIR)/stash.h \ $(OBJDIR)/stat_.c:$(OBJDIR)/stat.h \ $(OBJDIR)/statrep_.c:$(OBJDIR)/statrep.h \ $(OBJDIR)/style_.c:$(OBJDIR)/style.h \ @@ -800,10 +807,11 @@ $(OBJDIR)/user_.c:$(OBJDIR)/user.h \ $(OBJDIR)/utf8_.c:$(OBJDIR)/utf8.h \ $(OBJDIR)/util_.c:$(OBJDIR)/util.h \ $(OBJDIR)/verify_.c:$(OBJDIR)/verify.h \ $(OBJDIR)/vfile_.c:$(OBJDIR)/vfile.h \ + $(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \ $(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \ $(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \ $(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \ $(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \ $(OBJDIR)/wysiwyg_.c:$(OBJDIR)/wysiwyg.h \ @@ -1614,10 +1622,18 @@ $(OBJDIR)/skins.o: $(OBJDIR)/skins_.c $(OBJDIR)/skins.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/skins.o -c $(OBJDIR)/skins_.c $(OBJDIR)/skins.h: $(OBJDIR)/headers + +$(OBJDIR)/smtp_.c: $(SRCDIR)/smtp.c $(OBJDIR)/translate + $(OBJDIR)/translate $(SRCDIR)/smtp.c >$@ + +$(OBJDIR)/smtp.o: $(OBJDIR)/smtp_.c $(OBJDIR)/smtp.h $(SRCDIR)/config.h + $(XTCC) -o $(OBJDIR)/smtp.o -c $(OBJDIR)/smtp_.c + +$(OBJDIR)/smtp.h: $(OBJDIR)/headers $(OBJDIR)/sqlcmd_.c: $(SRCDIR)/sqlcmd.c $(OBJDIR)/translate $(OBJDIR)/translate $(SRCDIR)/sqlcmd.c >$@ $(OBJDIR)/sqlcmd.o: $(OBJDIR)/sqlcmd_.c $(OBJDIR)/sqlcmd.h $(SRCDIR)/config.h @@ -1790,10 +1806,18 @@ $(OBJDIR)/vfile.o: $(OBJDIR)/vfile_.c $(OBJDIR)/vfile.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/vfile.o -c $(OBJDIR)/vfile_.c $(OBJDIR)/vfile.h: $(OBJDIR)/headers + +$(OBJDIR)/webmail_.c: $(SRCDIR)/webmail.c $(OBJDIR)/translate + $(OBJDIR)/translate $(SRCDIR)/webmail.c >$@ + +$(OBJDIR)/webmail.o: $(OBJDIR)/webmail_.c $(OBJDIR)/webmail.h $(SRCDIR)/config.h + $(XTCC) -o $(OBJDIR)/webmail.o -c $(OBJDIR)/webmail_.c + +$(OBJDIR)/webmail.h: $(OBJDIR)/headers $(OBJDIR)/wiki_.c: $(SRCDIR)/wiki.c $(OBJDIR)/translate $(OBJDIR)/translate $(SRCDIR)/wiki.c >$@ $(OBJDIR)/wiki.o: $(OBJDIR)/wiki_.c $(OBJDIR)/wiki.h $(SRCDIR)/config.h Index: src/makemake.tcl ================================================================== --- src/makemake.tcl +++ src/makemake.tcl @@ -124,10 +124,11 @@ sha1hard sha3 shun sitemap skins + smtp sqlcmd stash stat statrep style @@ -146,10 +147,11 @@ user utf8 util verify vfile + webmail wiki wikiformat winfile winhttp wysiwyg Index: src/setup.c ================================================================== --- src/setup.c +++ src/setup.c @@ -114,11 +114,14 @@ "Configure the trouble-ticketing system for this repository"); setup_menu_entry("Search","srchsetup", "Configure the built-in search engine"); setup_menu_entry("URL Aliases", "waliassetup", "Configure URL aliases"); - setup_menu_entry("Email", "setup_email", "Email notifications"); + setup_menu_entry("Notification", "setup_notification", + "Automatic notifications of changes via outbound email"); + setup_menu_entry("Email-Server", "setup_smtp", + "Activate and configure the built-in email server"); setup_menu_entry("Transfers", "xfersetup", "Configure the transfer system for this repository"); setup_menu_entry("Skins", "setup_skin", "Select and/or modify the web interface \"skins\""); setup_menu_entry("Moderation", "setup_modreq", ADDED src/smtp.c Index: src/smtp.c ================================================================== --- /dev/null +++ src/smtp.c @@ -0,0 +1,1046 @@ +/* +** Copyright (c) 2018 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program 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. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** Implementation of SMTP (Simple Mail Transport Protocol) according +** to RFC 5321. +*/ +#include "config.h" +#include "smtp.h" +#include + +#ifdef __linux__ +# define FOSSIL_ENABLE_DNS_LOOKUP +#endif + +#if defined(FOSSIL_ENABLE_DNS_LOOKUP) +# include +# include +# include +# include +#endif /* defined(FOSSIL_ENABLE_DNS_LOOKUP) */ + + +/* +** Find the hostname for receiving email for the domain given +** in zDomain. Return NULL if not found or not implemented. +** If multiple email receivers are advertized, pick the one with +** the lowest preference number. +** +** The returned string is obtained from fossil_malloc() +** and should be released using fossil_free(). +*/ +char *smtp_mx_host(const char *zDomain){ +#if defined(FOSSIL_ENABLE_DNS_LOOKUP) + int nDns; /* Length of the DNS reply */ + int rc; /* Return code from various APIs */ + int i; /* Loop counter */ + int iBestPriority = 9999999; /* Best priority */ + int nRec; /* Number of answers */ + ns_msg h; /* DNS reply parser */ + const unsigned char *pBest = 0; /* RDATA for the best answer */ + unsigned char aDns[5000]; /* Raw DNS reply content */ + char zHostname[5000]; /* Hostname for the MX */ + + nDns = res_query(zDomain, C_IN, T_MX, aDns, sizeof(aDns)); + if( nDns<=0 ) return 0; + res_init(); + rc = ns_initparse(aDns,nDns,&h); + if( rc ) return 0; + nRec = ns_msg_count(h, ns_s_an); + for(i=0; i2 ){ + priority = p[0]*256 + p[1]; + if( priorityinbuf); + fossil_free(pSession->zHostname); + fossil_free(pSession->zErr); + fossil_free(pSession); +} + +/* +** Allocate a new SmtpSession object. +** +** Both zFrom and zDest must be specified. +** +** The ... arguments are in this order: +** +** SMTP_PORT: int +** SMTP_TRACE_FILE: FILE* +** SMTP_TRACE_BLOB: Blob* +*/ +SmtpSession *smtp_session_new( + const char *zFrom, /* Domain for the client */ + const char *zDest, /* Domain of the server */ + u32 smtpFlags, /* Flags */ + ... /* Arguments depending on the flags */ +){ + SmtpSession *p; + va_list ap; + 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); + va_start(ap, smtpFlags); + if( smtpFlags & SMTP_PORT ){ + url.port = va_arg(ap, int); + } + if( smtpFlags & SMTP_TRACE_FILE ){ + p->logFile = va_arg(ap, FILE*); + } + if( smtpFlags & SMTP_TRACE_BLOB ){ + p->pTranscript = va_arg(ap, Blob*); + } + va_end(ap); + 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 ){ + p->atEof = 1; + p->zErr = mprintf("cannot locate SMTP server for \"%s\"", zDest); + return p; + } + url.name = p->zHostname; + socket_global_init(); + if( socket_open(&url) ){ + p->atEof = 1; + p->zErr = socket_errmsg(); + socket_close(); + } + return p; +} + +/* +** 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' ); + assert( z[n-2]=='\r' ); + if( p->smtpFlags & SMTP_TRACE_STDOUT ){ + fossil_print("C: %.*s\n", n-2, z); + } + if( p->smtpFlags & SMTP_TRACE_FILE ){ + fprintf(p->logFile, "C: %.*s\n", n-2, z); + } + if( p->smtpFlags & SMTP_TRACE_BLOB ){ + blob_appendf(p->pTranscript, "C: %.*s\n", n-2, z); + } + socket_send(0, z, n); + blob_reset(&b); +} + +/* +** Read a line of input received from the SMTP server. Make in point +** to the next input line. +** +** Content is actually read into the p->in buffer. Then blob_line() +** is used to extract individual lines, passing each to "in". +*/ +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( iinbuf, 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; + } + do{ + size_t got; + blob_resize(&p->inbuf, n+1000); + z = blob_buffer(&p->inbuf); + got = socket_receive(0, z+n, 1000, 1); + if( got>0 ){ + in->nUsed += got; + n += got; + z[n] = 0; + if( n>0 && z[n-1]=='\n' ) break; + if( got==1000 ) continue; + } + nDelay++; + if( nDelay>100 ){ + blob_init(in, 0, 0); + p->zErr = mprintf("timeout"); + socket_close(); + p->atEof = 1; + return; + }else{ + sqlite3_sleep(100); + } + }while( n<1 || z[n-1]!='\n' ); + blob_truncate(&p->inbuf, n); + blob_line(&p->inbuf, in); + } + z = blob_buffer(in); + n = blob_size(in); + if( n && z[n-1]=='\n' ) n--; + if( n && z[n-1]=='\r' ) n--; + if( p->smtpFlags & SMTP_TRACE_STDOUT ){ + fossil_print("S: %.*s\n", n, z); + } + if( p->smtpFlags & SMTP_TRACE_FILE ){ + fprintf(p->logFile, "S: %.*s\n", n, z); + } + if( p->smtpFlags & SMTP_TRACE_BLOB ){ + blob_appendf(p->pTranscript, "S: %.*s\n", n-2, z); + } +} + +/* +** Capture a single-line server reply. +*/ +static void smtp_get_reply_from_server( + SmtpSession *p, /* The SMTP connection */ + Blob *in, /* Buffer used to hold the reply */ + int *piCode, /* The return code */ + 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{ + *piCode = atoi(z); + *pbMore = n>=4 && z[3]=='-'; + *pzArg = n>=4 ? z+4 : ""; + } +} + +/* +** 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; + 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; + do{ + smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); + }while( bMore ); + if( iCode!=220 ){ + 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_client_quit(p); + return 1; + } + return 0; +} + +/* +** COMMAND: test-smtp-probe +** +** Usage: %fossil test-smtp-probe DOMAIN [ME] +** +** Interact with the SMTP server for DOMAIN by setting up a connection +** and then immediately shutting it back down. Log all interaction +** on the console. Use ME as the domain name of the sender. +** +** Options: +** +** --direct Use DOMAIN directly without going through MX +** --port N Talk on TCP port N +*/ +void test_smtp_probe(void){ + SmtpSession *p; + const char *zDomain; + const char *zSelf; + const char *zPort; + int iPort = 25; + u32 smtpFlags = SMTP_TRACE_STDOUT|SMTP_PORT; + + if( find_option("direct",0,0)!=0 ) smtpFlags |= SMTP_DIRECT; + zPort = find_option("port",0,1); + if( zPort ) iPort = atoi(zPort); + verify_all_options(); + if( g.argc!=3 && g.argc!=4 ) usage("DOMAIN [ME]"); + zDomain = g.argv[2]; + zSelf = g.argc==4 ? g.argv[3] : "fossil-scm.org"; + p = smtp_session_new(zSelf, zDomain, smtpFlags, iPort); + if( p->zErr ){ + fossil_fatal("%s", p->zErr); + } + fossil_print("Connection to \"%s\"\n", p->zHostname); + smtp_client_startup(p); + smtp_client_quit(p); + if( p->zErr ){ + fossil_fatal("ERROR: %s\n", p->zErr); + } + smtp_session_free(p); +} + +/* +** Send the content of an email message followed by a single +** "." line. All lines must be \r\n terminated. Any isolated +** \n line terminators in the input must be converted. Also, +** an line contain using "." should be converted to "..". +*/ +static void smtp_send_email_body( + const char *zMsg, /* Message to send */ + size_t (*xSend)(void*,const void*,size_t), /* Sender callback function */ + void *pArg /* First arg to sender */ +){ + Blob in; + Blob out = BLOB_INITIALIZER; + Blob line; + blob_init(&in, zMsg, -1); + while( blob_line(&in, &line) ){ + char *z = blob_buffer(&line); + int n = blob_size(&line); + if( n==0 ) break; + n--; + if( n && z[n-1]=='\r' ) n--; + if( n==1 && z[0]=='.' ){ + blob_append(&out, "..\r\n", 4); + }else{ + blob_append(&out, z, n); + blob_append(&out, "\r\n", 2); + } + } + blob_append(&out, ".\r\n", 3); + xSend(pArg, blob_buffer(&out), blob_size(&out)); + blob_reset(&out); + blob_reset(&line); +} + +/* A sender function appropriate for use by smtp_send_email_body() to +** send all content to the console, for testing. +*/ +static size_t smtp_test_sender(void *NotUsed, const void *pContent, size_t N){ + return fwrite(pContent, 1, N, stdout); +} + +/* +** COMMAND: test-smtp-senddata +** +** Usage: %fossil test-smtp-senddata FILE +** +** Read content from FILE, then send it to stdout encoded as if sent +** to the DATA portion of an SMTP session. This command is used to +** test the encoding logic. +*/ +void test_smtp_senddata(void){ + Blob f; + if( g.argc!=3 ) usage("FILE"); + blob_read_from_file(&f, g.argv[2], ExtFILE); + smtp_send_email_body(blob_str(&f), smtp_test_sender, 0); + blob_reset(&f); +} + +/* +** Send a single email message to the SMTP server. +** +** All email addresses (zFrom and azTo) must be plain "local@domain" +** format without the surrounding "<..>". This routine will add the +** necessary "<..>". +** +** The body of the email should be well-structured. This routine will +** convert any \n line endings into \r\n and will escape lines containing +** just ".", but will not make any other alterations or corrections to +** the message content. +** +** Return 0 on success. Otherwise an error code. +*/ +int smtp_send_msg( + SmtpSession *p, /* The SMTP server to which the message is sent */ + const char *zFrom, /* Who the message is from */ + int nTo, /* Number of receipients */ + const char **azTo, /* Email address of each recipient */ + const char *zMsg /* Body of the message */ +){ + int i; + int iCode = 0; + int bMore = 0; + char *zArg = 0; + Blob in; + blob_init(&in, 0, 0); + 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 ) return 1; + for(i=0; i\r\n", azTo[i]); + do{ + smtp_get_reply_from_server(p, &in, &iCode, &bMore, &zArg); + }while( bMore ); + if( iCode!=250 ) 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 ) 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 ) return 1; + return 0; +} + +/* +** The input is a base email address of the form "local@domain". +** Return a pointer to just the "domain" part. +*/ +static const char *domainOfAddr(const char *z){ + while( z[0] && z[0]!='@' ) z++; + if( z[0]==0 ) return 0; + return z+1; +} + + +/* +** COMMAND: test-smtp-send +** +** Usage: %fossil test-smtp-send EMAIL FROM TO ... +** +** Use SMTP to send the email message contained in the file named EMAIL +** to the list of users TO. FROM is the sender of the email. +** +** Options: +** +** --direct Go directly to the TO domain. Bypass MX lookup +** --port N Use TCP port N instead of 25 +** --trace Show the SMTP conversation on the console +*/ +void test_smtp_send(void){ + SmtpSession *p; + const char *zFrom; + int nTo; + const char *zToDomain; + const char *zFromDomain; + const char **azTo; + int smtpPort = 25; + const char *zPort; + Blob body; + u32 smtpFlags = SMTP_PORT; + if( find_option("trace",0,0)!=0 ) smtpFlags |= SMTP_TRACE_STDOUT; + if( find_option("direct",0,0)!=0 ) smtpFlags |= SMTP_DIRECT; + zPort = find_option("port",0,1); + if( zPort ) smtpPort = atoi(zPort); + verify_all_options(); + if( g.argc<5 ) usage("EMAIL FROM TO ..."); + blob_read_from_file(&body, g.argv[2], ExtFILE); + zFrom = g.argv[3]; + nTo = g.argc-4; + azTo = (const char**)g.argv+4; + zFromDomain = domainOfAddr(zFrom); + zToDomain = domainOfAddr(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); + 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); +} + +/***************************************************************************** +** Server implementation +*****************************************************************************/ + +/* +** Schema used by the email processing system. +*/ +static const char zEmailSchema[] = +@ -- bulk storage is in a separate table. This table can store either +@ -- the body of email messages or transcripts of smtp sessions. +@ CREATE TABLE IF NOT EXISTS repository.emailblob( +@ emailid INTEGER PRIMARY KEY, -- numeric idea for the entry +@ ets INT, -- Corresponding transcript, or NULL +@ etime INT, -- insertion time, secs since 1970 +@ etxt TEXT -- content of this entry +@ ); +@ +@ -- One row for each mailbox entry. All users emails are stored in +@ -- this same table. +@ CREATE TABLE IF NOT EXISTS repository.emailbox( +@ euser TEXT, -- User who received this email +@ edate INT, -- Date received. Seconds since 1970 +@ efrom TEXT, -- Who is the email from +@ emsgid INT, -- Raw email text +@ ets INT, -- Transcript of the receiving SMTP session +@ estate INT, -- Unread, read, starred, etc. +@ esubject TEXT -- Subject line for display +@ ); +@ +@ -- Information on how to deliver incoming email. +@ CREATE TABLE IF NOT EXISTS repository.emailroute( +@ eaddr TEXT PRIMARY KEY, -- Email address +@ epolicy TEXT -- How to handle email sent to this address +@ ) WITHOUT ROWID; +@ +@ -- Outgoing email queue +@ CREATE TABLE IF NOT EXISTS repository.emailoutq( +@ edomain TEXT, -- Destination domain. (ex: "fossil-scm.org") +@ efrom TEXT, -- Sender email address +@ eto TEXT, -- Receipient email address +@ emsgid INT, -- Message body in the emailblob table +@ ectime INT, -- Time enqueued. Seconds since 1970 +@ emtime INT, -- Time of last send attempt. Sec since 1970 +@ ensend INT, -- Number of send attempts +@ ets INT -- Transcript of last failed attempt +@ ); +; + +/* +** Code used to delete the email tables. +*/ +static const char zEmailDrop[] = +@ DROP TABLE IF EXISTS emailblob; +@ DROP TABLE IF EXISTS emailbox; +@ DROP TABLE IF EXISTS emailroute; +@ DROP TABLE IF EXISTS emailqueue; +; + +/* +** Populate the schema of a database. +** +** eForce==0 Fast +** eForce==1 Run CREATE TABLE statements every time +** eForce==2 DROP then rerun CREATE TABLE +*/ +void smtp_server_schema(int eForce){ + if( eForce==2 ){ + db_multi_exec(zEmailDrop/*works-like:""*/); + } + if( eForce==1 || !db_table_exists("repository","emailblob") ){ + db_multi_exec(zEmailSchema/*works-like:""*/); + } +} + +/* +** WEBPAGE: setup_smtp +** +** Administrative page for configuring and controlling inbound email and +** output email queuing. This page is available to administrators +** only via the /Admin/EmailServer menu. +*/ +void setup_smtp(void){ + login_check_credentials(); + if( !g.perm.Setup ){ + login_needed(0); + return; + } + style_header("Email Server Setup"); + @ Pending... + style_footer(); +} + + +#if LOCAL_INTERFACE +/* +** State information for the server +*/ +struct SmtpServer { + sqlite3_int64 idTranscript; /* Transcript ID number */ + sqlite3_int64 idMsg; /* Message ID number */ + char *zEhlo; /* Client domain on the EHLO line */ + char *zFrom; /* MAIL FROM: argument */ + int nTo; /* Number of RCPT TO: lines seen */ + struct SmtpTo { + char *z; /* Address in each RCPT TO line */ + int okRemote; /* zTo can be in another domain */ + } *aTo; + u32 srvrFlags; /* Control flags */ + Blob msg; /* Content following DATA */ + Blob transcript; /* Session transcript */ +}; + +#define SMTPSRV_CLEAR_MSG 1 /* smtp_server_clear() last message only */ +#define SMTPSRV_CLEAR_ALL 2 /* smtp_server_clear() everything */ +#define SMTPSRV_LOG 0x001 /* Record a transcript of the interaction */ +#define SMTPSRV_STDERR 0x002 /* Transcription written to stderr */ +#define SMTPSRV_DRYRUN 0x004 /* Do not record anything in database */ + +#endif /* LOCAL_INTERFACE */ + +/* +** Clear the SmtpServer object. Deallocate resources. +** How much to clear depends on eHowMuch +*/ +static void smtp_server_clear(SmtpServer *p, int eHowMuch){ + int i; + if( eHowMuch>=SMTPSRV_CLEAR_MSG ){ + fossil_free(p->zFrom); + p->zFrom = 0; + for(i=0; inTo; i++) fossil_free(p->aTo[i].z); + fossil_free(p->aTo); + p->aTo = 0; + p->nTo = 0; + blob_reset(&p->msg); + p->idMsg = 0; + } + if( eHowMuch>=SMTPSRV_CLEAR_ALL ){ + blob_reset(&p->transcript); + p->idTranscript = 0; + fossil_free(p->zEhlo); + p->zEhlo = 0; + } +} + +/* +** Turn raw memory into an SmtpServer object. +*/ +static void smtp_server_init(SmtpServer *p){ + memset(p, 0, sizeof(*p)); + blob_init(&p->msg, 0, 0); + blob_init(&p->transcript, 0, 0); +} + +/* +** Append a new TO entry to the SmtpServer object. Do not do the +** append if the same entry is already on the list. +** +** The zAddr argument is obtained from fossil_malloc(). This +** routine assumes ownership of the allocation. +*/ +static void smtp_append_to(SmtpServer *p, char *zAddr, int okRemote){ + int i; + for(i=0; zAddr[i]; i++){ zAddr[i] = fossil_tolower(zAddr[i]); } + for(i=0; inTo; i++){ + if( strcmp(zAddr, p->aTo[i].z)==0 ){ + fossil_free(zAddr); + if( p->aTo[i].okRemote==0 ) p->aTo[i].okRemote = okRemote; + return; + } + } + p->aTo = fossil_realloc(p->aTo, (p->nTo+1)*sizeof(p->aTo[0])); + p->aTo[p->nTo].z = zAddr; + p->aTo[p->nTo].okRemote = okRemote; + p->nTo++; +} + +/* +** Send a single line of output from the server to the client. +*/ +static void smtp_server_send(SmtpServer *p, const char *zFormat, ...){ + Blob b = empty_blob; + va_list ap; + char *z; + int n; + 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' ); + assert( z[n-2]=='\r' ); + if( p->srvrFlags & SMTPSRV_LOG ){ + blob_appendf(&p->transcript, "S: %.*s\n", n-2, z); + } + if( p->srvrFlags & SMTPSRV_STDERR ){ + fprintf(stderr, "S: %.*s\n", n-2, z); + } + fwrite(z, n, 1, stdout); + fflush(stdout); + blob_reset(&b); +} + +/* +** Read a single line from the client. +*/ +static int smtp_server_gets(SmtpServer *p, char *aBuf, int nBuf){ + int rc = fgets(aBuf, nBuf, stdin)!=0; + if( rc ){ + if( (p->srvrFlags & SMTPSRV_LOG)!=0 ){ + blob_appendf(&p->transcript, "C: %s", aBuf); + } + if( (p->srvrFlags & SMTPSRV_STDERR)!=0 ){ + fprintf(stderr, "C: %s", aBuf); + } + } + return rc; +} + +/* +** Capture the incoming email data into the p->msg blob. Dequote +** lines of "..\r\n" into just ".\r\n". +*/ +static void smtp_server_capture_data(SmtpServer *p, char *z, int n){ + int nLine = 0; + while( fgets(z, n, stdin) ){ + if( strncmp(z, ".\r\n", 3)==0 || strncmp(z, ".\n",2)==0 ) break; + nLine++; + if( strncmp(z, "..\r\n", 4)==0 || strncmp(z, "..\n",3)==0 ){ + memmove(z, z+1, 4); + } + blob_append(&p->msg, z, -1); + } + if( p->srvrFlags & SMTPSRV_LOG ){ + blob_appendf(&p->transcript, "C: # %d lines, %d bytes of content\n", + nLine, blob_size(&p->msg)); + } + if( p->srvrFlags & SMTPSRV_STDERR ){ + fprintf(stderr, "C: # %d lines, %d bytes of content\n", + nLine, blob_size(&p->msg)); + } +} + +/* +** Send an email to a single email addess that is registered with +** this system, according to the instructions in emailroute. If +** zAddr is not in the emailroute table, then this routine is a +** no-op. Or if zAddr has already been processed, then this +** routine is a no-op. +*/ +static void smtp_server_send_one_user( + SmtpServer *p, /* The current inbound email */ + const char *zAddr, /* Who to forward this to */ + int okRemote /* True if ok to foward to another domain */ +){ + char *zPolicy; + Blob policy, line, token, tail; + + zPolicy = db_text(0, + "SELECT epolicy FROM emailroute WHERE eaddr=%Q", zAddr); + if( zPolicy==0 ){ + if( okRemote ){ + int i; + for(i=0; zAddr[i] && zAddr[i]!='@'; i++){} + if( zAddr[i]=='@' && zAddr[i+1]!=0 ){ + db_multi_exec( + "INSERT INTO emailoutq(edomain,efrom,eto,emsgid,ectime," + "emtime,ensend)" + "VALUES(%Q,%Q,%Q,%lld,now(),0,0)", + zAddr+i+1, p->zFrom, zAddr, p->idMsg + ); + } + } + return; + } + blob_init(&policy, zPolicy, -1); + while( blob_line(&policy, &line) ){ + blob_trim(&line); + blob_token(&line, &token); + blob_tail(&line, &tail); + if( blob_size(&tail)==0 ) continue; + if( blob_eq_str(&token, "mbox", 4) ){ + Blob subj; + email_header_value(&p->msg, "subject", &subj); + db_multi_exec( + "INSERT INTO emailbox(euser,edate,efrom,emsgid,ets,estate,esubject)" + " VALUES(%Q,now(),%Q,%lld,%lld,0,%Q)", + blob_str(&tail), p->zFrom, p->idMsg, p->idTranscript, + blob_str(&subj) + ); + blob_reset(&subj); + } + if( blob_eq_str(&token, "forward", 7) ){ + smtp_append_to(p, fossil_strdup(blob_str(&tail)), 1); + } + blob_reset(&tail); + } +} + +/* +** The SmtpServer object contains a complete incoming email. +** Add this email to the database. +*/ +static void smtp_server_route_incoming(SmtpServer *p, int bFinish){ + Stmt s; + int i; + if( p->zFrom + && p->nTo + && blob_size(&p->msg) + && (p->srvrFlags & SMTPSRV_DRYRUN)==0 + ){ + db_begin_transaction(); + if( p->idTranscript==0 ) smtp_server_schema(0); + db_prepare(&s, + "INSERT INTO emailblob(ets,etime,etxt)" + " VALUES(:ets,now(),compress(:etxt))" + ); + if( !bFinish && p->idTranscript==0 ){ + db_bind_null(&s, ":ets"); + db_bind_null(&s, ":etxt"); + db_step(&s); + db_reset(&s); + p->idTranscript = db_last_insert_rowid(); + }else if( bFinish ){ + if( p->idTranscript ){ + db_multi_exec( + "UPDATE emailblob SET etxt=compress(%Q)" + " WHERE emailid=%lld", + blob_str(&p->transcript), p->idTranscript); + }else{ + db_bind_null(&s, ":ets"); + db_bind_str(&s, ":etxt", &p->transcript); + db_step(&s); + db_reset(&s); + p->idTranscript = db_last_insert_rowid(); + } + } + db_bind_int64(&s, ":ets", p->idTranscript); + db_bind_str(&s, ":etxt", &p->msg); + db_step(&s); + db_finalize(&s); + p->idMsg = db_last_insert_rowid(); + + /* make entries in emailbox and emailoutq */ + for(i=0; inTo; i++){ + int okRemote = p->aTo[i].okRemote; + p->aTo[i].okRemote = 1; + smtp_server_send_one_user(p, p->aTo[i].z, okRemote); + } + + /* Finish the transaction after all changes are implemented */ + db_end_transaction(0); + } + smtp_server_clear(p, SMTPSRV_CLEAR_MSG); +} + +/* +** COMMAND: smtpd +** +** Usage: %fossil smtpd [OPTIONS] REPOSITORY +** +** Begin a SMTP conversation with a client using stdin/stdout. The +** received email is stored in REPOSITORY. +** +** Options: +** +** --dryrun Do not record any emails in the database +** +** --trace Print a transcript of the conversation on stderr +** for debugging and analysis +*/ +void smtp_server(void){ + char *zDbName; + const char *zDomain; + SmtpServer x; + char z[5000]; + + smtp_server_init(&x); + zDomain = find_option("domain",0,1); + if( zDomain==0 ) zDomain = ""; + x.srvrFlags = SMTPSRV_LOG; + if( find_option("trace",0,0)!=0 ) x.srvrFlags |= SMTPSRV_STDERR; + if( find_option("dryrun",0,0)!=0 ) x.srvrFlags |= SMTPSRV_DRYRUN; + verify_all_options(); + if( g.argc!=3 ) usage("DBNAME"); + zDbName = g.argv[2]; + zDbName = enter_chroot_jail(zDbName, 0); + db_open_repository(zDbName); + add_content_sql_commands(g.db); + smtp_server_send(&x, "220 %s ESMTP https://fossil-scm.org/ %s\r\n", + zDomain, MANIFEST_VERSION); + while( smtp_server_gets(&x, z, sizeof(z)) ){ + if( strncmp(z, "EHLO", 4)==0 && fossil_isspace(z[4]) ){ + smtp_server_send(&x, "250 ok\r\n"); + }else + if( strncmp(z, "HELO", 4)==0 && fossil_isspace(z[4]) ){ + smtp_server_send(&x, "250 ok\r\n"); + }else + if( strncmp(z, "MAIL FROM:<", 11)==0 ){ + smtp_server_route_incoming(&x, 0); + smtp_server_clear(&x, SMTPSRV_CLEAR_MSG); + x.zFrom = email_copy_addr(z+11); + if( x.zFrom==0 ){ + smtp_server_send(&x, "500 unacceptable email address\r\n"); + }else{ + smtp_server_send(&x, "250 ok\r\n"); + } + }else + if( strncmp(z, "RCPT TO:<", 9)==0 ){ + char *zAddr; + if( x.zFrom==0 ){ + smtp_server_send(&x, "500 missing MAIL FROM\r\n"); + continue; + } + zAddr = email_copy_addr(z+9); + if( zAddr==0 ){ + smtp_server_send(&x, "505 no such user\r\n"); + continue; + } + smtp_append_to(&x, zAddr, 0); + if( x.nTo>=100 ){ + smtp_server_send(&x, "452 too many recipients\r\n"); + continue; + } + smtp_server_send(&x, "250 ok\r\n"); + }else + if( strncmp(z, "DATA", 4)==0 && fossil_isspace(z[4]) ){ + if( x.zFrom==0 || x.nTo==0 ){ + smtp_server_send(&x, "500 missing RCPT TO\r\n"); + continue; + } + smtp_server_send(&x, "354 ready\r\n"); + smtp_server_capture_data(&x, z, sizeof(z)); + smtp_server_send(&x, "250 ok\r\n"); + }else + if( strncmp(z, "QUIT", 4)==0 && fossil_isspace(z[4]) ){ + smtp_server_send(&x, "221 closing connection\r\n"); + smtp_server_route_incoming(&x, 1); + break; + }else + { + smtp_server_send(&x, "500 unknown command\r\n"); + } + } + smtp_server_clear(&x, SMTPSRV_CLEAR_ALL); +} ADDED src/webmail.c Index: src/webmail.c ================================================================== --- /dev/null +++ src/webmail.c @@ -0,0 +1,116 @@ +/* +** Copyright (c) 2018 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program 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. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** Implementation of web pages for managing the email storage tables +** (if they exist): +** +** emailbox +** emailblob +** emailroute +*/ +#include "config.h" +#include "webmail.h" +#include + +/* +** WEBPAGE: webmail +** +** This page can be used to read content from the EMAILBOX table +** that contains email received by the "fossil smtpd" command. +*/ +void webmail_page(void){ + int emailid; + Stmt q; + Blob sql; + int showAll = 0; + login_check_credentials(); + if( g.zLogin==0 ){ + login_needed(0); + return; + } + if( !db_table_exists("repository","emailbox") ){ + style_header("Webmail Not Available"); + @

This repository is not configured to provide webmail

+ style_footer(); + return; + } + add_content_sql_commands(g.db); + emailid = atoi(PD("id","0")); + if( emailid>0 ){ + blob_init(&sql, 0, 0); + blob_append_sql(&sql, "SELECT decompress(etxt)" + " FROM emailblob WHERE emailid=%d", + emailid); + if( !g.perm.Admin ){ + blob_append_sql(&sql, " AND EXISTS(SELECT 1 FROM emailbox WHERE" + " euser=%Q AND emsgid=emailid)", g.zLogin); + } + db_prepare_blob(&q, &sql); + blob_reset(&sql); + if( db_step(&q)==SQLITE_ROW ){ + style_header("Message %d",emailid); + @
%h(db_column_text(&q, 0))
+ style_footer(); + db_finalize(&q); + return; + } + db_finalize(&q); + } + style_header("Webmail"); + blob_init(&sql, 0, 0); + blob_append_sql(&sql, + /* 0 1 2 3 4 5 */ + "SELECT efrom, datetime(edate,'unixepoch'), estate, esubject, emsgid, euser" + " FROM emailbox" + ); + if( g.perm.Admin ){ + const char *zUser = P("user"); + if( P("all")!=0 ){ + /* Show all email messages */ + showAll = 1; + }else{ + style_submenu_element("All", "%R/webmail?all"); + if( zUser ){ + blob_append_sql(&sql, " WHERE euser=%Q", zUser); + }else{ + blob_append_sql(&sql, " WHERE euser=%Q", g.zLogin); + } + } + }else{ + blob_append_sql(&sql, " WHERE euser=%Q", g.zLogin); + } + blob_append_sql(&sql, " ORDER BY edate DESC limit 50"); + db_prepare_blob(&q, &sql); + blob_reset(&sql); + @
    + while( db_step(&q)==SQLITE_ROW ){ + int emailid = db_column_int(&q,4); + const char *zFrom = db_column_text(&q, 0); + const char *zDate = db_column_text(&q, 1); + const char *zSubject = db_column_text(&q, 3); + @
  1. + if( showAll ){ + const char *zTo = db_column_text(&q,5); + @ %h(zTo): + } + @ %h(zFrom) → %h(zSubject) + @ %h(zDate) + } + db_finalize(&q); + @
+ style_footer(); +} Index: win/Makefile.dmc ================================================================== --- win/Makefile.dmc +++ win/Makefile.dmc @@ -28,13 +28,13 @@ SQLITE_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB SHELL_OPTIONS = -DNDEBUG=1 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_OMIT_DECLTYPE -DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_GET_TABLE -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_LOCKING_STYLE=0 -DSQLITE_DEFAULT_FILE_FORMAT=4 -DSQLITE_ENABLE_EXPLAIN_COMMENTS -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_ENABLE_JSON1 -DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_STMTVTAB -DSQLITE_HAVE_ZLIB -DSQLITE_INTROSPECTION_PRAGMAS -DSQLITE_ENABLE_DBPAGE_VTAB -Dmain=sqlite3_shell -DSQLITE_SHELL_IS_UTF8=1 -DSQLITE_OMIT_LOAD_EXTENSION=1 -DUSE_SYSTEM_SQLITE=$(USE_SYSTEM_SQLITE) -DSQLITE_SHELL_DBNAME_PROC=sqlcmd_get_dbname -DSQLITE_SHELL_INIT_PROC=sqlcmd_init_proc -Daccess=file_access -Dsystem=fossil_system -Dgetenv=fossil_getenv -Dfopen=fossil_fopen -SRC = add_.c allrepo_.c attach_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c +SRC = add_.c allrepo_.c attach_.c bag_.c bisect_.c blob_.c branch_.c browse_.c builtin_.c bundle_.c cache_.c captcha_.c cgi_.c checkin_.c checkout_.c clearsign_.c clone_.c comformat_.c configure_.c content_.c cookies_.c db_.c delta_.c deltacmd_.c descendants_.c diff_.c diffcmd_.c dispatch_.c doc_.c email_.c encode_.c etag_.c event_.c export_.c file_.c finfo_.c foci_.c forum_.c fshell_.c fusefs_.c glob_.c graph_.c gzip_.c hname_.c http_.c http_socket_.c http_ssl_.c http_transport_.c import_.c info_.c json_.c json_artifact_.c json_branch_.c json_config_.c json_diff_.c json_dir_.c json_finfo_.c json_login_.c json_query_.c json_report_.c json_status_.c json_tag_.c json_timeline_.c json_user_.c json_wiki_.c leaf_.c loadctrl_.c login_.c lookslike_.c main_.c manifest_.c markdown_.c markdown_html_.c md5_.c merge_.c merge3_.c moderate_.c name_.c path_.c piechart_.c pivot_.c popen_.c pqueue_.c printf_.c publish_.c purge_.c rebuild_.c regexp_.c report_.c rss_.c schema_.c search_.c security_audit_.c setup_.c sha1_.c sha1hard_.c sha3_.c shun_.c sitemap_.c skins_.c smtp_.c sqlcmd_.c stash_.c stat_.c statrep_.c style_.c sync_.c tag_.c tar_.c th_main_.c timeline_.c tkt_.c tktsetup_.c undo_.c unicode_.c unversioned_.c update_.c url_.c user_.c utf8_.c util_.c verify_.c vfile_.c webmail_.c wiki_.c wikiformat_.c winfile_.c winhttp_.c wysiwyg_.c xfer_.c xfersetup_.c zip_.c -OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O +OBJ = $(OBJDIR)\add$O $(OBJDIR)\allrepo$O $(OBJDIR)\attach$O $(OBJDIR)\bag$O $(OBJDIR)\bisect$O $(OBJDIR)\blob$O $(OBJDIR)\branch$O $(OBJDIR)\browse$O $(OBJDIR)\builtin$O $(OBJDIR)\bundle$O $(OBJDIR)\cache$O $(OBJDIR)\captcha$O $(OBJDIR)\cgi$O $(OBJDIR)\checkin$O $(OBJDIR)\checkout$O $(OBJDIR)\clearsign$O $(OBJDIR)\clone$O $(OBJDIR)\comformat$O $(OBJDIR)\configure$O $(OBJDIR)\content$O $(OBJDIR)\cookies$O $(OBJDIR)\db$O $(OBJDIR)\delta$O $(OBJDIR)\deltacmd$O $(OBJDIR)\descendants$O $(OBJDIR)\diff$O $(OBJDIR)\diffcmd$O $(OBJDIR)\dispatch$O $(OBJDIR)\doc$O $(OBJDIR)\email$O $(OBJDIR)\encode$O $(OBJDIR)\etag$O $(OBJDIR)\event$O $(OBJDIR)\export$O $(OBJDIR)\file$O $(OBJDIR)\finfo$O $(OBJDIR)\foci$O $(OBJDIR)\forum$O $(OBJDIR)\fshell$O $(OBJDIR)\fusefs$O $(OBJDIR)\glob$O $(OBJDIR)\graph$O $(OBJDIR)\gzip$O $(OBJDIR)\hname$O $(OBJDIR)\http$O $(OBJDIR)\http_socket$O $(OBJDIR)\http_ssl$O $(OBJDIR)\http_transport$O $(OBJDIR)\import$O $(OBJDIR)\info$O $(OBJDIR)\json$O $(OBJDIR)\json_artifact$O $(OBJDIR)\json_branch$O $(OBJDIR)\json_config$O $(OBJDIR)\json_diff$O $(OBJDIR)\json_dir$O $(OBJDIR)\json_finfo$O $(OBJDIR)\json_login$O $(OBJDIR)\json_query$O $(OBJDIR)\json_report$O $(OBJDIR)\json_status$O $(OBJDIR)\json_tag$O $(OBJDIR)\json_timeline$O $(OBJDIR)\json_user$O $(OBJDIR)\json_wiki$O $(OBJDIR)\leaf$O $(OBJDIR)\loadctrl$O $(OBJDIR)\login$O $(OBJDIR)\lookslike$O $(OBJDIR)\main$O $(OBJDIR)\manifest$O $(OBJDIR)\markdown$O $(OBJDIR)\markdown_html$O $(OBJDIR)\md5$O $(OBJDIR)\merge$O $(OBJDIR)\merge3$O $(OBJDIR)\moderate$O $(OBJDIR)\name$O $(OBJDIR)\path$O $(OBJDIR)\piechart$O $(OBJDIR)\pivot$O $(OBJDIR)\popen$O $(OBJDIR)\pqueue$O $(OBJDIR)\printf$O $(OBJDIR)\publish$O $(OBJDIR)\purge$O $(OBJDIR)\rebuild$O $(OBJDIR)\regexp$O $(OBJDIR)\report$O $(OBJDIR)\rss$O $(OBJDIR)\schema$O $(OBJDIR)\search$O $(OBJDIR)\security_audit$O $(OBJDIR)\setup$O $(OBJDIR)\sha1$O $(OBJDIR)\sha1hard$O $(OBJDIR)\sha3$O $(OBJDIR)\shun$O $(OBJDIR)\sitemap$O $(OBJDIR)\skins$O $(OBJDIR)\smtp$O $(OBJDIR)\sqlcmd$O $(OBJDIR)\stash$O $(OBJDIR)\stat$O $(OBJDIR)\statrep$O $(OBJDIR)\style$O $(OBJDIR)\sync$O $(OBJDIR)\tag$O $(OBJDIR)\tar$O $(OBJDIR)\th_main$O $(OBJDIR)\timeline$O $(OBJDIR)\tkt$O $(OBJDIR)\tktsetup$O $(OBJDIR)\undo$O $(OBJDIR)\unicode$O $(OBJDIR)\unversioned$O $(OBJDIR)\update$O $(OBJDIR)\url$O $(OBJDIR)\user$O $(OBJDIR)\utf8$O $(OBJDIR)\util$O $(OBJDIR)\verify$O $(OBJDIR)\vfile$O $(OBJDIR)\webmail$O $(OBJDIR)\wiki$O $(OBJDIR)\wikiformat$O $(OBJDIR)\winfile$O $(OBJDIR)\winhttp$O $(OBJDIR)\wysiwyg$O $(OBJDIR)\xfer$O $(OBJDIR)\xfersetup$O $(OBJDIR)\zip$O $(OBJDIR)\shell$O $(OBJDIR)\sqlite3$O $(OBJDIR)\th$O $(OBJDIR)\th_lang$O RC=$(DMDIR)\bin\rcc RCFLAGS=-32 -w1 -I$(SRCDIR) /D__DMC__ @@ -49,11 +49,11 @@ $(OBJDIR)\fossil.res: $B\win\fossil.rc $(RC) $(RCFLAGS) -o$@ $** $(OBJDIR)\link: $B\win\Makefile.dmc $(OBJDIR)\fossil.res - +echo add allrepo attach bag bisect blob branch browse builtin bundle cache captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ + +echo add allrepo attach bag bisect blob branch browse builtin bundle cache captcha cgi checkin checkout clearsign clone comformat configure content cookies db delta deltacmd descendants diff diffcmd dispatch doc email encode etag event export file finfo foci forum fshell fusefs glob graph gzip hname http http_socket http_ssl http_transport import info json json_artifact json_branch json_config json_diff json_dir json_finfo json_login json_query json_report json_status json_tag json_timeline json_user json_wiki leaf loadctrl login lookslike main manifest markdown markdown_html md5 merge merge3 moderate name path piechart pivot popen pqueue printf publish purge rebuild regexp report rss schema search security_audit setup sha1 sha1hard sha3 shun sitemap skins smtp sqlcmd stash stat statrep style sync tag tar th_main timeline tkt tktsetup undo unicode unversioned update url user utf8 util verify vfile webmail wiki wikiformat winfile winhttp wysiwyg xfer xfersetup zip shell sqlite3 th th_lang > $@ +echo fossil >> $@ +echo fossil >> $@ +echo $(LIBS) >> $@ +echo. >> $@ +echo fossil >> $@ @@ -728,10 +728,16 @@ $(OBJDIR)\skins$O : skins_.c skins.h $(TCC) -o$@ -c skins_.c skins_.c : $(SRCDIR)\skins.c +translate$E $** > $@ + +$(OBJDIR)\smtp$O : smtp_.c smtp.h + $(TCC) -o$@ -c smtp_.c + +smtp_.c : $(SRCDIR)\smtp.c + +translate$E $** > $@ $(OBJDIR)\sqlcmd$O : sqlcmd_.c sqlcmd.h $(TCC) -o$@ -c sqlcmd_.c sqlcmd_.c : $(SRCDIR)\sqlcmd.c @@ -860,10 +866,16 @@ $(OBJDIR)\vfile$O : vfile_.c vfile.h $(TCC) -o$@ -c vfile_.c vfile_.c : $(SRCDIR)\vfile.c +translate$E $** > $@ + +$(OBJDIR)\webmail$O : webmail_.c webmail.h + $(TCC) -o$@ -c webmail_.c + +webmail_.c : $(SRCDIR)\webmail.c + +translate$E $** > $@ $(OBJDIR)\wiki$O : wiki_.c wiki.h $(TCC) -o$@ -c wiki_.c wiki_.c : $(SRCDIR)\wiki.c @@ -910,7 +922,7 @@ zip_.c : $(SRCDIR)\zip.c +translate$E $** > $@ headers: makeheaders$E page_index.h builtin_data.h default_css.h VERSION.h - +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h + +makeheaders$E add_.c:add.h allrepo_.c:allrepo.h attach_.c:attach.h bag_.c:bag.h bisect_.c:bisect.h blob_.c:blob.h branch_.c:branch.h browse_.c:browse.h builtin_.c:builtin.h bundle_.c:bundle.h cache_.c:cache.h captcha_.c:captcha.h cgi_.c:cgi.h checkin_.c:checkin.h checkout_.c:checkout.h clearsign_.c:clearsign.h clone_.c:clone.h comformat_.c:comformat.h configure_.c:configure.h content_.c:content.h cookies_.c:cookies.h db_.c:db.h delta_.c:delta.h deltacmd_.c:deltacmd.h descendants_.c:descendants.h diff_.c:diff.h diffcmd_.c:diffcmd.h dispatch_.c:dispatch.h doc_.c:doc.h email_.c:email.h encode_.c:encode.h etag_.c:etag.h event_.c:event.h export_.c:export.h file_.c:file.h finfo_.c:finfo.h foci_.c:foci.h forum_.c:forum.h fshell_.c:fshell.h fusefs_.c:fusefs.h glob_.c:glob.h graph_.c:graph.h gzip_.c:gzip.h hname_.c:hname.h http_.c:http.h http_socket_.c:http_socket.h http_ssl_.c:http_ssl.h http_transport_.c:http_transport.h import_.c:import.h info_.c:info.h json_.c:json.h json_artifact_.c:json_artifact.h json_branch_.c:json_branch.h json_config_.c:json_config.h json_diff_.c:json_diff.h json_dir_.c:json_dir.h json_finfo_.c:json_finfo.h json_login_.c:json_login.h json_query_.c:json_query.h json_report_.c:json_report.h json_status_.c:json_status.h json_tag_.c:json_tag.h json_timeline_.c:json_timeline.h json_user_.c:json_user.h json_wiki_.c:json_wiki.h leaf_.c:leaf.h loadctrl_.c:loadctrl.h login_.c:login.h lookslike_.c:lookslike.h main_.c:main.h manifest_.c:manifest.h markdown_.c:markdown.h markdown_html_.c:markdown_html.h md5_.c:md5.h merge_.c:merge.h merge3_.c:merge3.h moderate_.c:moderate.h name_.c:name.h path_.c:path.h piechart_.c:piechart.h pivot_.c:pivot.h popen_.c:popen.h pqueue_.c:pqueue.h printf_.c:printf.h publish_.c:publish.h purge_.c:purge.h rebuild_.c:rebuild.h regexp_.c:regexp.h report_.c:report.h rss_.c:rss.h schema_.c:schema.h search_.c:search.h security_audit_.c:security_audit.h setup_.c:setup.h sha1_.c:sha1.h sha1hard_.c:sha1hard.h sha3_.c:sha3.h shun_.c:shun.h sitemap_.c:sitemap.h skins_.c:skins.h smtp_.c:smtp.h sqlcmd_.c:sqlcmd.h stash_.c:stash.h stat_.c:stat.h statrep_.c:statrep.h style_.c:style.h sync_.c:sync.h tag_.c:tag.h tar_.c:tar.h th_main_.c:th_main.h timeline_.c:timeline.h tkt_.c:tkt.h tktsetup_.c:tktsetup.h undo_.c:undo.h unicode_.c:unicode.h unversioned_.c:unversioned.h update_.c:update.h url_.c:url.h user_.c:user.h utf8_.c:utf8.h util_.c:util.h verify_.c:verify.h vfile_.c:vfile.h webmail_.c:webmail.h wiki_.c:wiki.h wikiformat_.c:wikiformat.h winfile_.c:winfile.h winhttp_.c:winhttp.h wysiwyg_.c:wysiwyg.h xfer_.c:xfer.h xfersetup_.c:xfersetup.h zip_.c:zip.h $(SRCDIR)\sqlite3.h $(SRCDIR)\th.h VERSION.h $(SRCDIR)\cson_amalgamation.h @copy /Y nul: headers Index: win/Makefile.mingw ================================================================== --- win/Makefile.mingw +++ win/Makefile.mingw @@ -532,10 +532,11 @@ $(SRCDIR)/sha1hard.c \ $(SRCDIR)/sha3.c \ $(SRCDIR)/shun.c \ $(SRCDIR)/sitemap.c \ $(SRCDIR)/skins.c \ + $(SRCDIR)/smtp.c \ $(SRCDIR)/sqlcmd.c \ $(SRCDIR)/stash.c \ $(SRCDIR)/stat.c \ $(SRCDIR)/statrep.c \ $(SRCDIR)/style.c \ @@ -554,10 +555,11 @@ $(SRCDIR)/user.c \ $(SRCDIR)/utf8.c \ $(SRCDIR)/util.c \ $(SRCDIR)/verify.c \ $(SRCDIR)/vfile.c \ + $(SRCDIR)/webmail.c \ $(SRCDIR)/wiki.c \ $(SRCDIR)/wikiformat.c \ $(SRCDIR)/winfile.c \ $(SRCDIR)/winhttp.c \ $(SRCDIR)/wysiwyg.c \ @@ -735,10 +737,11 @@ $(OBJDIR)/sha1hard_.c \ $(OBJDIR)/sha3_.c \ $(OBJDIR)/shun_.c \ $(OBJDIR)/sitemap_.c \ $(OBJDIR)/skins_.c \ + $(OBJDIR)/smtp_.c \ $(OBJDIR)/sqlcmd_.c \ $(OBJDIR)/stash_.c \ $(OBJDIR)/stat_.c \ $(OBJDIR)/statrep_.c \ $(OBJDIR)/style_.c \ @@ -757,10 +760,11 @@ $(OBJDIR)/user_.c \ $(OBJDIR)/utf8_.c \ $(OBJDIR)/util_.c \ $(OBJDIR)/verify_.c \ $(OBJDIR)/vfile_.c \ + $(OBJDIR)/webmail_.c \ $(OBJDIR)/wiki_.c \ $(OBJDIR)/wikiformat_.c \ $(OBJDIR)/winfile_.c \ $(OBJDIR)/winhttp_.c \ $(OBJDIR)/wysiwyg_.c \ @@ -867,10 +871,11 @@ $(OBJDIR)/sha1hard.o \ $(OBJDIR)/sha3.o \ $(OBJDIR)/shun.o \ $(OBJDIR)/sitemap.o \ $(OBJDIR)/skins.o \ + $(OBJDIR)/smtp.o \ $(OBJDIR)/sqlcmd.o \ $(OBJDIR)/stash.o \ $(OBJDIR)/stat.o \ $(OBJDIR)/statrep.o \ $(OBJDIR)/style.o \ @@ -889,10 +894,11 @@ $(OBJDIR)/user.o \ $(OBJDIR)/utf8.o \ $(OBJDIR)/util.o \ $(OBJDIR)/verify.o \ $(OBJDIR)/vfile.o \ + $(OBJDIR)/webmail.o \ $(OBJDIR)/wiki.o \ $(OBJDIR)/wikiformat.o \ $(OBJDIR)/winfile.o \ $(OBJDIR)/winhttp.o \ $(OBJDIR)/wysiwyg.o \ @@ -1218,10 +1224,11 @@ $(OBJDIR)/sha1hard_.c:$(OBJDIR)/sha1hard.h \ $(OBJDIR)/sha3_.c:$(OBJDIR)/sha3.h \ $(OBJDIR)/shun_.c:$(OBJDIR)/shun.h \ $(OBJDIR)/sitemap_.c:$(OBJDIR)/sitemap.h \ $(OBJDIR)/skins_.c:$(OBJDIR)/skins.h \ + $(OBJDIR)/smtp_.c:$(OBJDIR)/smtp.h \ $(OBJDIR)/sqlcmd_.c:$(OBJDIR)/sqlcmd.h \ $(OBJDIR)/stash_.c:$(OBJDIR)/stash.h \ $(OBJDIR)/stat_.c:$(OBJDIR)/stat.h \ $(OBJDIR)/statrep_.c:$(OBJDIR)/statrep.h \ $(OBJDIR)/style_.c:$(OBJDIR)/style.h \ @@ -1240,10 +1247,11 @@ $(OBJDIR)/user_.c:$(OBJDIR)/user.h \ $(OBJDIR)/utf8_.c:$(OBJDIR)/utf8.h \ $(OBJDIR)/util_.c:$(OBJDIR)/util.h \ $(OBJDIR)/verify_.c:$(OBJDIR)/verify.h \ $(OBJDIR)/vfile_.c:$(OBJDIR)/vfile.h \ + $(OBJDIR)/webmail_.c:$(OBJDIR)/webmail.h \ $(OBJDIR)/wiki_.c:$(OBJDIR)/wiki.h \ $(OBJDIR)/wikiformat_.c:$(OBJDIR)/wikiformat.h \ $(OBJDIR)/winfile_.c:$(OBJDIR)/winfile.h \ $(OBJDIR)/winhttp_.c:$(OBJDIR)/winhttp.h \ $(OBJDIR)/wysiwyg_.c:$(OBJDIR)/wysiwyg.h \ @@ -2056,10 +2064,18 @@ $(OBJDIR)/skins.o: $(OBJDIR)/skins_.c $(OBJDIR)/skins.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/skins.o -c $(OBJDIR)/skins_.c $(OBJDIR)/skins.h: $(OBJDIR)/headers + +$(OBJDIR)/smtp_.c: $(SRCDIR)/smtp.c $(TRANSLATE) + $(TRANSLATE) $(SRCDIR)/smtp.c >$@ + +$(OBJDIR)/smtp.o: $(OBJDIR)/smtp_.c $(OBJDIR)/smtp.h $(SRCDIR)/config.h + $(XTCC) -o $(OBJDIR)/smtp.o -c $(OBJDIR)/smtp_.c + +$(OBJDIR)/smtp.h: $(OBJDIR)/headers $(OBJDIR)/sqlcmd_.c: $(SRCDIR)/sqlcmd.c $(TRANSLATE) $(TRANSLATE) $(SRCDIR)/sqlcmd.c >$@ $(OBJDIR)/sqlcmd.o: $(OBJDIR)/sqlcmd_.c $(OBJDIR)/sqlcmd.h $(SRCDIR)/config.h @@ -2232,10 +2248,18 @@ $(OBJDIR)/vfile.o: $(OBJDIR)/vfile_.c $(OBJDIR)/vfile.h $(SRCDIR)/config.h $(XTCC) -o $(OBJDIR)/vfile.o -c $(OBJDIR)/vfile_.c $(OBJDIR)/vfile.h: $(OBJDIR)/headers + +$(OBJDIR)/webmail_.c: $(SRCDIR)/webmail.c $(TRANSLATE) + $(TRANSLATE) $(SRCDIR)/webmail.c >$@ + +$(OBJDIR)/webmail.o: $(OBJDIR)/webmail_.c $(OBJDIR)/webmail.h $(SRCDIR)/config.h + $(XTCC) -o $(OBJDIR)/webmail.o -c $(OBJDIR)/webmail_.c + +$(OBJDIR)/webmail.h: $(OBJDIR)/headers $(OBJDIR)/wiki_.c: $(SRCDIR)/wiki.c $(TRANSLATE) $(TRANSLATE) $(SRCDIR)/wiki.c >$@ $(OBJDIR)/wiki.o: $(OBJDIR)/wiki_.c $(OBJDIR)/wiki.h $(SRCDIR)/config.h Index: win/Makefile.msc ================================================================== --- win/Makefile.msc +++ win/Makefile.msc @@ -478,10 +478,11 @@ sha1hard_.c \ sha3_.c \ shun_.c \ sitemap_.c \ skins_.c \ + smtp_.c \ sqlcmd_.c \ stash_.c \ stat_.c \ statrep_.c \ style_.c \ @@ -500,10 +501,11 @@ user_.c \ utf8_.c \ util_.c \ verify_.c \ vfile_.c \ + webmail_.c \ wiki_.c \ wikiformat_.c \ winfile_.c \ winhttp_.c \ wysiwyg_.c \ @@ -681,10 +683,11 @@ $(OX)\sha3$O \ $(OX)\shell$O \ $(OX)\shun$O \ $(OX)\sitemap$O \ $(OX)\skins$O \ + $(OX)\smtp$O \ $(OX)\sqlcmd$O \ $(OX)\sqlite3$O \ $(OX)\stash$O \ $(OX)\stat$O \ $(OX)\statrep$O \ @@ -707,10 +710,11 @@ $(OX)\user$O \ $(OX)\utf8$O \ $(OX)\util$O \ $(OX)\verify$O \ $(OX)\vfile$O \ + $(OX)\webmail$O \ $(OX)\wiki$O \ $(OX)\wikiformat$O \ $(OX)\winfile$O \ $(OX)\winhttp$O \ $(OX)\wysiwyg$O \ @@ -872,10 +876,11 @@ echo $(OX)\sha3.obj >> $@ echo $(OX)\shell.obj >> $@ echo $(OX)\shun.obj >> $@ echo $(OX)\sitemap.obj >> $@ echo $(OX)\skins.obj >> $@ + echo $(OX)\smtp.obj >> $@ echo $(OX)\sqlcmd.obj >> $@ echo $(OX)\sqlite3.obj >> $@ echo $(OX)\stash.obj >> $@ echo $(OX)\stat.obj >> $@ echo $(OX)\statrep.obj >> $@ @@ -898,10 +903,11 @@ echo $(OX)\user.obj >> $@ echo $(OX)\utf8.obj >> $@ echo $(OX)\util.obj >> $@ echo $(OX)\verify.obj >> $@ echo $(OX)\vfile.obj >> $@ + echo $(OX)\webmail.obj >> $@ echo $(OX)\wiki.obj >> $@ echo $(OX)\wikiformat.obj >> $@ echo $(OX)\winfile.obj >> $@ echo $(OX)\winhttp.obj >> $@ echo $(OX)\wysiwyg.obj >> $@ @@ -1623,10 +1629,16 @@ $(OX)\skins$O : skins_.c skins.h $(TCC) /Fo$@ -c skins_.c skins_.c : $(SRCDIR)\skins.c translate$E $** > $@ + +$(OX)\smtp$O : smtp_.c smtp.h + $(TCC) /Fo$@ -c smtp_.c + +smtp_.c : $(SRCDIR)\smtp.c + translate$E $** > $@ $(OX)\sqlcmd$O : sqlcmd_.c sqlcmd.h $(TCC) /Fo$@ -c sqlcmd_.c sqlcmd_.c : $(SRCDIR)\sqlcmd.c @@ -1755,10 +1767,16 @@ $(OX)\vfile$O : vfile_.c vfile.h $(TCC) /Fo$@ -c vfile_.c vfile_.c : $(SRCDIR)\vfile.c translate$E $** > $@ + +$(OX)\webmail$O : webmail_.c webmail.h + $(TCC) /Fo$@ -c webmail_.c + +webmail_.c : $(SRCDIR)\webmail.c + translate$E $** > $@ $(OX)\wiki$O : wiki_.c wiki.h $(TCC) /Fo$@ -c wiki_.c wiki_.c : $(SRCDIR)\wiki.c @@ -1908,10 +1926,11 @@ sha1hard_.c:sha1hard.h \ sha3_.c:sha3.h \ shun_.c:shun.h \ sitemap_.c:sitemap.h \ skins_.c:skins.h \ + smtp_.c:smtp.h \ sqlcmd_.c:sqlcmd.h \ stash_.c:stash.h \ stat_.c:stat.h \ statrep_.c:statrep.h \ style_.c:style.h \ @@ -1930,10 +1949,11 @@ user_.c:user.h \ utf8_.c:utf8.h \ util_.c:util.h \ verify_.c:verify.h \ vfile_.c:vfile.h \ + webmail_.c:webmail.h \ wiki_.c:wiki.h \ wikiformat_.c:wikiformat.h \ winfile_.c:winfile.h \ winhttp_.c:winhttp.h \ wysiwyg_.c:wysiwyg.h \