Fossil SCM

Merge in support for email notifications. Munch more work is needed, but it seems ready to begin functional testing.

drh 2018-06-23 00:53 trunk merge
Commit b77f1aaed000fbe4ae36118a57a65c47153c1c816e76bc3e7de4e65f29ba3c93
+1 -1
--- src/attach.c
+++ src/attach.c
@@ -377,11 +377,11 @@
377377
}
378378
if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop);
379379
if( P("cancel") ){
380380
cgi_redirect(zFrom);
381381
}
382
- if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){
382
+ if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
383383
int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
384384
(zPage!=0 && wiki_need_moderation(0));
385385
const char *zComment = PD("comment", "");
386386
attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
387387
cgi_redirect(zFrom);
388388
--- src/attach.c
+++ src/attach.c
@@ -377,11 +377,11 @@
377 }
378 if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop);
379 if( P("cancel") ){
380 cgi_redirect(zFrom);
381 }
382 if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct()) ){
383 int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
384 (zPage!=0 && wiki_need_moderation(0));
385 const char *zComment = PD("comment", "");
386 attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
387 cgi_redirect(zFrom);
388
--- src/attach.c
+++ src/attach.c
@@ -377,11 +377,11 @@
377 }
378 if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop);
379 if( P("cancel") ){
380 cgi_redirect(zFrom);
381 }
382 if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
383 int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
384 (zPage!=0 && wiki_need_moderation(0));
385 const char *zComment = PD("comment", "");
386 attach_commit(zName, zTarget, aContent, szContent, needModerator, zComment);
387 cgi_redirect(zFrom);
388
+17
--- src/blob.c
+++ src/blob.c
@@ -480,10 +480,17 @@
480480
** Rewind the cursor on a blob back to the beginning.
481481
*/
482482
void blob_rewind(Blob *p){
483483
p->iCursor = 0;
484484
}
485
+
486
+/*
487
+** Truncate a blob back to zero length
488
+*/
489
+void blob_truncate(Blob *p){
490
+ p->nUsed = 0;
491
+}
485492
486493
/*
487494
** Seek the cursor in a blob to the indicated offset.
488495
*/
489496
int blob_seek(Blob *p, int offset, int whence){
@@ -653,10 +660,20 @@
653660
if( pTo ){
654661
blob_append(pTo, &pFrom->aData[pFrom->iCursor], i - pFrom->iCursor);
655662
}
656663
pFrom->iCursor = i;
657664
}
665
+
666
+/*
667
+** Ensure that the text in pBlob ends with '\n'
668
+*/
669
+void blob_add_final_newline(Blob *pBlob){
670
+ if( pBlob->nUsed<=0 ) return;
671
+ if( pBlob->aData[pBlob->nUsed-1]!='\n' ){
672
+ blob_append(pBlob, "\n", 1);
673
+ }
674
+}
658675
659676
/*
660677
** Return true if the blob contains a valid base16 identifier artifact hash.
661678
**
662679
** The value returned is actually one of HNAME_SHA1 OR HNAME_K256 if the
663680
--- src/blob.c
+++ src/blob.c
@@ -480,10 +480,17 @@
480 ** Rewind the cursor on a blob back to the beginning.
481 */
482 void blob_rewind(Blob *p){
483 p->iCursor = 0;
484 }
 
 
 
 
 
 
 
485
486 /*
487 ** Seek the cursor in a blob to the indicated offset.
488 */
489 int blob_seek(Blob *p, int offset, int whence){
@@ -653,10 +660,20 @@
653 if( pTo ){
654 blob_append(pTo, &pFrom->aData[pFrom->iCursor], i - pFrom->iCursor);
655 }
656 pFrom->iCursor = i;
657 }
 
 
 
 
 
 
 
 
 
 
658
659 /*
660 ** Return true if the blob contains a valid base16 identifier artifact hash.
661 **
662 ** The value returned is actually one of HNAME_SHA1 OR HNAME_K256 if the
663
--- src/blob.c
+++ src/blob.c
@@ -480,10 +480,17 @@
480 ** Rewind the cursor on a blob back to the beginning.
481 */
482 void blob_rewind(Blob *p){
483 p->iCursor = 0;
484 }
485
486 /*
487 ** Truncate a blob back to zero length
488 */
489 void blob_truncate(Blob *p){
490 p->nUsed = 0;
491 }
492
493 /*
494 ** Seek the cursor in a blob to the indicated offset.
495 */
496 int blob_seek(Blob *p, int offset, int whence){
@@ -653,10 +660,20 @@
660 if( pTo ){
661 blob_append(pTo, &pFrom->aData[pFrom->iCursor], i - pFrom->iCursor);
662 }
663 pFrom->iCursor = i;
664 }
665
666 /*
667 ** Ensure that the text in pBlob ends with '\n'
668 */
669 void blob_add_final_newline(Blob *pBlob){
670 if( pBlob->nUsed<=0 ) return;
671 if( pBlob->aData[pBlob->nUsed-1]!='\n' ){
672 blob_append(pBlob, "\n", 1);
673 }
674 }
675
676 /*
677 ** Return true if the blob contains a valid base16 identifier artifact hash.
678 **
679 ** The value returned is actually one of HNAME_SHA1 OR HNAME_K256 if the
680
+3 -3
--- src/captcha.c
+++ src/captcha.c
@@ -497,17 +497,17 @@
497497
** true (non-zero).
498498
**
499499
** The query parameters examined are "captchaseed" for the seed value and
500500
** "captcha" for text that the user types in response to the captcha prompt.
501501
*/
502
-int captcha_is_correct(void){
502
+int captcha_is_correct(int bAlwaysNeeded){
503503
const char *zSeed;
504504
const char *zEntered;
505505
const char *zDecode;
506506
char z[30];
507507
int i;
508
- if( !captcha_needed() ){
508
+ if( !bAlwaysNeeded && !captcha_needed() ){
509509
return 1; /* No captcha needed */
510510
}
511511
zSeed = P("captchaseed");
512512
if( zSeed==0 ) return 0;
513513
zEntered = P("captcha");
@@ -593,11 +593,11 @@
593593
}
594594
#endif
595595
zCookieName = mprintf("fossil-cc-%.10s", db_get("project-code","x"));
596596
zCookieValue = P(zCookieName);
597597
if( zCookieValue && atoi(zCookieValue)==1 ) return 0;
598
- if( captcha_is_correct() ){
598
+ if( captcha_is_correct(0) ){
599599
cgi_set_cookie(zCookieName, "1", login_cookie_path(), 8*3600);
600600
return 0;
601601
}
602602
603603
/* This appears to be a spider. Offer the captcha */
604604
--- src/captcha.c
+++ src/captcha.c
@@ -497,17 +497,17 @@
497 ** true (non-zero).
498 **
499 ** The query parameters examined are "captchaseed" for the seed value and
500 ** "captcha" for text that the user types in response to the captcha prompt.
501 */
502 int captcha_is_correct(void){
503 const char *zSeed;
504 const char *zEntered;
505 const char *zDecode;
506 char z[30];
507 int i;
508 if( !captcha_needed() ){
509 return 1; /* No captcha needed */
510 }
511 zSeed = P("captchaseed");
512 if( zSeed==0 ) return 0;
513 zEntered = P("captcha");
@@ -593,11 +593,11 @@
593 }
594 #endif
595 zCookieName = mprintf("fossil-cc-%.10s", db_get("project-code","x"));
596 zCookieValue = P(zCookieName);
597 if( zCookieValue && atoi(zCookieValue)==1 ) return 0;
598 if( captcha_is_correct() ){
599 cgi_set_cookie(zCookieName, "1", login_cookie_path(), 8*3600);
600 return 0;
601 }
602
603 /* This appears to be a spider. Offer the captcha */
604
--- src/captcha.c
+++ src/captcha.c
@@ -497,17 +497,17 @@
497 ** true (non-zero).
498 **
499 ** The query parameters examined are "captchaseed" for the seed value and
500 ** "captcha" for text that the user types in response to the captcha prompt.
501 */
502 int captcha_is_correct(int bAlwaysNeeded){
503 const char *zSeed;
504 const char *zEntered;
505 const char *zDecode;
506 char z[30];
507 int i;
508 if( !bAlwaysNeeded && !captcha_needed() ){
509 return 1; /* No captcha needed */
510 }
511 zSeed = P("captchaseed");
512 if( zSeed==0 ) return 0;
513 zEntered = P("captcha");
@@ -593,11 +593,11 @@
593 }
594 #endif
595 zCookieName = mprintf("fossil-cc-%.10s", db_get("project-code","x"));
596 zCookieValue = P(zCookieName);
597 if( zCookieValue && atoi(zCookieValue)==1 ) return 0;
598 if( captcha_is_correct(0) ){
599 cgi_set_cookie(zCookieName, "1", login_cookie_path(), 8*3600);
600 return 0;
601 }
602
603 /* This appears to be a spider. Offer the captcha */
604
+25
--- src/cgi.c
+++ src/cgi.c
@@ -56,10 +56,12 @@
5656
#define P(x) cgi_parameter((x),0)
5757
#define PD(x,y) cgi_parameter((x),(y))
5858
#define PT(x) cgi_parameter_trimmed((x),0)
5959
#define PDT(x,y) cgi_parameter_trimmed((x),(y))
6060
#define PB(x) cgi_parameter_boolean(x)
61
+#define PCK(x) cgi_parameter_checked(x,1)
62
+#define PIF(x,y) cgi_parameter_checked(x,y)
6163
6264
6365
/*
6466
** Destinations for output text.
6567
*/
@@ -1118,10 +1120,33 @@
11181120
int cgi_parameter_boolean(const char *zName){
11191121
const char *zIn = cgi_parameter(zName, 0);
11201122
if( zIn==0 ) return 0;
11211123
return zIn[0]==0 || is_truth(zIn);
11221124
}
1125
+
1126
+/*
1127
+** Return either an empty string "" or the string "checked" depending
1128
+** on whether or not parameter zName has value iValue. If parameter
1129
+** zName does not exist, that is assumed to be the same as value 0.
1130
+**
1131
+** This routine implements the PCK(x) and PIF(x,y) macros. The PIF(x,y)
1132
+** macro generateds " checked" if the value of parameter x equals integer y.
1133
+** PCK(x) is the same as PIF(x,1). These macros are used to generate
1134
+** the "checked" attribute on checkbox and radio controls of forms.
1135
+*/
1136
+const char *cgi_parameter_checked(const char *zName, int iValue){
1137
+ const char *zIn = cgi_parameter(zName,0);
1138
+ int x;
1139
+ if( zIn==0 ){
1140
+ x = 0;
1141
+ }else if( !fossil_isdigit(zIn[0]) ){
1142
+ x = is_truth(zIn);
1143
+ }else{
1144
+ x = atoi(zIn);
1145
+ }
1146
+ return x==iValue ? "checked" : "";
1147
+}
11231148
11241149
/*
11251150
** Return the name of the i-th CGI parameter. Return NULL if there
11261151
** are fewer than i registered CGI parameters.
11271152
*/
11281153
--- src/cgi.c
+++ src/cgi.c
@@ -56,10 +56,12 @@
56 #define P(x) cgi_parameter((x),0)
57 #define PD(x,y) cgi_parameter((x),(y))
58 #define PT(x) cgi_parameter_trimmed((x),0)
59 #define PDT(x,y) cgi_parameter_trimmed((x),(y))
60 #define PB(x) cgi_parameter_boolean(x)
 
 
61
62
63 /*
64 ** Destinations for output text.
65 */
@@ -1118,10 +1120,33 @@
1118 int cgi_parameter_boolean(const char *zName){
1119 const char *zIn = cgi_parameter(zName, 0);
1120 if( zIn==0 ) return 0;
1121 return zIn[0]==0 || is_truth(zIn);
1122 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1123
1124 /*
1125 ** Return the name of the i-th CGI parameter. Return NULL if there
1126 ** are fewer than i registered CGI parameters.
1127 */
1128
--- src/cgi.c
+++ src/cgi.c
@@ -56,10 +56,12 @@
56 #define P(x) cgi_parameter((x),0)
57 #define PD(x,y) cgi_parameter((x),(y))
58 #define PT(x) cgi_parameter_trimmed((x),0)
59 #define PDT(x,y) cgi_parameter_trimmed((x),(y))
60 #define PB(x) cgi_parameter_boolean(x)
61 #define PCK(x) cgi_parameter_checked(x,1)
62 #define PIF(x,y) cgi_parameter_checked(x,y)
63
64
65 /*
66 ** Destinations for output text.
67 */
@@ -1118,10 +1120,33 @@
1120 int cgi_parameter_boolean(const char *zName){
1121 const char *zIn = cgi_parameter(zName, 0);
1122 if( zIn==0 ) return 0;
1123 return zIn[0]==0 || is_truth(zIn);
1124 }
1125
1126 /*
1127 ** Return either an empty string "" or the string "checked" depending
1128 ** on whether or not parameter zName has value iValue. If parameter
1129 ** zName does not exist, that is assumed to be the same as value 0.
1130 **
1131 ** This routine implements the PCK(x) and PIF(x,y) macros. The PIF(x,y)
1132 ** macro generateds " checked" if the value of parameter x equals integer y.
1133 ** PCK(x) is the same as PIF(x,1). These macros are used to generate
1134 ** the "checked" attribute on checkbox and radio controls of forms.
1135 */
1136 const char *cgi_parameter_checked(const char *zName, int iValue){
1137 const char *zIn = cgi_parameter(zName,0);
1138 int x;
1139 if( zIn==0 ){
1140 x = 0;
1141 }else if( !fossil_isdigit(zIn[0]) ){
1142 x = is_truth(zIn);
1143 }else{
1144 x = atoi(zIn);
1145 }
1146 return x==iValue ? "checked" : "";
1147 }
1148
1149 /*
1150 ** Return the name of the i-th CGI parameter. Return NULL if there
1151 ** are fewer than i registered CGI parameters.
1152 */
1153
--- src/codecheck1.c
+++ src/codecheck1.c
@@ -331,11 +331,15 @@
331331
** Return true if the input is an argument that is never safe for use
332332
** with %s.
333333
*/
334334
static int never_safe(const char *z){
335335
if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0;
336
- if( z[0]=='P' ) return 1; /* CGI macros like P() and PD() */
336
+ if( z[0]=='P' ){
337
+ if( strncmp(z,"PIF(",4)==0 ) return 0;
338
+ if( strncmp(z,"PCK(",4)==0 ) return 0;
339
+ return 1;
340
+ }
337341
if( strncmp(z,"cgi_param",9)==0 ) return 1;
338342
return 0;
339343
}
340344
341345
/*
342346
--- src/codecheck1.c
+++ src/codecheck1.c
@@ -331,11 +331,15 @@
331 ** Return true if the input is an argument that is never safe for use
332 ** with %s.
333 */
334 static int never_safe(const char *z){
335 if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0;
336 if( z[0]=='P' ) return 1; /* CGI macros like P() and PD() */
 
 
 
 
337 if( strncmp(z,"cgi_param",9)==0 ) return 1;
338 return 0;
339 }
340
341 /*
342
--- src/codecheck1.c
+++ src/codecheck1.c
@@ -331,11 +331,15 @@
331 ** Return true if the input is an argument that is never safe for use
332 ** with %s.
333 */
334 static int never_safe(const char *z){
335 if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0;
336 if( z[0]=='P' ){
337 if( strncmp(z,"PIF(",4)==0 ) return 0;
338 if( strncmp(z,"PCK(",4)==0 ) return 0;
339 return 1;
340 }
341 if( strncmp(z,"cgi_param",9)==0 ) return 1;
342 return 0;
343 }
344
345 /*
346
+46
--- src/db.c
+++ src/db.c
@@ -310,10 +310,31 @@
310310
db.pAllStmt = pStmt;
311311
va_end(ap);
312312
}
313313
return rc;
314314
}
315
+
316
+/* Prepare a statement using text placed inside a Blob
317
+** using blob_append_sql().
318
+*/
319
+int db_prepare_blob(Stmt *pStmt, Blob *pSql){
320
+ int rc;
321
+ char *zSql;
322
+ pStmt->sql = *pSql;
323
+ blob_init(pSql, 0, 0);
324
+ zSql = blob_sql_text(&pStmt->sql);
325
+ db.nPrepare++;
326
+ rc = sqlite3_prepare_v3(g.db, zSql, -1, 0, &pStmt->pStmt, 0);
327
+ if( rc!=0 ){
328
+ db_err("%s\n%s", sqlite3_errmsg(g.db), zSql);
329
+ }
330
+ pStmt->pNext = pStmt->pPrev = 0;
331
+ pStmt->nStep = 0;
332
+ pStmt->rc = rc;
333
+ return rc;
334
+}
335
+
315336
316337
/*
317338
** Return the index of a bind parameter
318339
*/
319340
static int paramIdx(Stmt *pStmt, const char *zParamName){
@@ -875,10 +896,33 @@
875896
}else{
876897
sqlite3_result_text(context, "utc", -1, SQLITE_STATIC);
877898
}
878899
}
879900
901
+/*
902
+** If the input is a hexadecimal string, convert that string into a BLOB.
903
+** If the input is not a hexadecimal string, return NULL.
904
+*/
905
+void db_hextoblob(
906
+ sqlite3_context *context,
907
+ int argc,
908
+ sqlite3_value **argv
909
+){
910
+ const unsigned char *zIn = sqlite3_value_text(argv[0]);
911
+ int nIn = sqlite3_value_bytes(argv[0]);
912
+ unsigned char *zOut;
913
+ if( zIn==0 ) return;
914
+ if( nIn&1 ) return;
915
+ if( !validate16((const char*)zIn, nIn) ) return;
916
+ zOut = sqlite3_malloc64( nIn/2 );
917
+ if( zOut==0 ){
918
+ sqlite3_result_error_nomem(context);
919
+ return;
920
+ }
921
+ decode16(zIn, zOut, nIn);
922
+ sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free);
923
+}
880924
881925
/*
882926
** Register the SQL functions that are useful both to the internal
883927
** representation and to the "fossil sql" command.
884928
*/
@@ -893,10 +937,12 @@
893937
db_now_function, 0, 0);
894938
sqlite3_create_function(db, "toLocal", 0, SQLITE_UTF8, 0,
895939
db_tolocal_function, 0, 0);
896940
sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0,
897941
db_fromlocal_function, 0, 0);
942
+ sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0,
943
+ db_hextoblob, 0, 0);
898944
}
899945
900946
#if USE_SEE
901947
/*
902948
** This is a pointer to the saved database encryption key string.
903949
--- src/db.c
+++ src/db.c
@@ -310,10 +310,31 @@
310 db.pAllStmt = pStmt;
311 va_end(ap);
312 }
313 return rc;
314 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
316 /*
317 ** Return the index of a bind parameter
318 */
319 static int paramIdx(Stmt *pStmt, const char *zParamName){
@@ -875,10 +896,33 @@
875 }else{
876 sqlite3_result_text(context, "utc", -1, SQLITE_STATIC);
877 }
878 }
879
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
880
881 /*
882 ** Register the SQL functions that are useful both to the internal
883 ** representation and to the "fossil sql" command.
884 */
@@ -893,10 +937,12 @@
893 db_now_function, 0, 0);
894 sqlite3_create_function(db, "toLocal", 0, SQLITE_UTF8, 0,
895 db_tolocal_function, 0, 0);
896 sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0,
897 db_fromlocal_function, 0, 0);
 
 
898 }
899
900 #if USE_SEE
901 /*
902 ** This is a pointer to the saved database encryption key string.
903
--- src/db.c
+++ src/db.c
@@ -310,10 +310,31 @@
310 db.pAllStmt = pStmt;
311 va_end(ap);
312 }
313 return rc;
314 }
315
316 /* Prepare a statement using text placed inside a Blob
317 ** using blob_append_sql().
318 */
319 int db_prepare_blob(Stmt *pStmt, Blob *pSql){
320 int rc;
321 char *zSql;
322 pStmt->sql = *pSql;
323 blob_init(pSql, 0, 0);
324 zSql = blob_sql_text(&pStmt->sql);
325 db.nPrepare++;
326 rc = sqlite3_prepare_v3(g.db, zSql, -1, 0, &pStmt->pStmt, 0);
327 if( rc!=0 ){
328 db_err("%s\n%s", sqlite3_errmsg(g.db), zSql);
329 }
330 pStmt->pNext = pStmt->pPrev = 0;
331 pStmt->nStep = 0;
332 pStmt->rc = rc;
333 return rc;
334 }
335
336
337 /*
338 ** Return the index of a bind parameter
339 */
340 static int paramIdx(Stmt *pStmt, const char *zParamName){
@@ -875,10 +896,33 @@
896 }else{
897 sqlite3_result_text(context, "utc", -1, SQLITE_STATIC);
898 }
899 }
900
901 /*
902 ** If the input is a hexadecimal string, convert that string into a BLOB.
903 ** If the input is not a hexadecimal string, return NULL.
904 */
905 void db_hextoblob(
906 sqlite3_context *context,
907 int argc,
908 sqlite3_value **argv
909 ){
910 const unsigned char *zIn = sqlite3_value_text(argv[0]);
911 int nIn = sqlite3_value_bytes(argv[0]);
912 unsigned char *zOut;
913 if( zIn==0 ) return;
914 if( nIn&1 ) return;
915 if( !validate16((const char*)zIn, nIn) ) return;
916 zOut = sqlite3_malloc64( nIn/2 );
917 if( zOut==0 ){
918 sqlite3_result_error_nomem(context);
919 return;
920 }
921 decode16(zIn, zOut, nIn);
922 sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free);
923 }
924
925 /*
926 ** Register the SQL functions that are useful both to the internal
927 ** representation and to the "fossil sql" command.
928 */
@@ -893,10 +937,12 @@
937 db_now_function, 0, 0);
938 sqlite3_create_function(db, "toLocal", 0, SQLITE_UTF8, 0,
939 db_tolocal_function, 0, 0);
940 sqlite3_create_function(db, "fromLocal", 0, SQLITE_UTF8, 0,
941 db_fromlocal_function, 0, 0);
942 sqlite3_create_function(db, "hextoblob", 1, SQLITE_UTF8, 0,
943 db_hextoblob, 0, 0);
944 }
945
946 #if USE_SEE
947 /*
948 ** This is a pointer to the saved database encryption key string.
949
+1165 -84
--- src/email.c
+++ src/email.c
@@ -19,10 +19,15 @@
1919
*/
2020
#include "config.h"
2121
#include "email.h"
2222
#include <assert.h>
2323
24
+/*
25
+** Maximum size of the subscriberCode blob, in bytes
26
+*/
27
+#define SUBSCRIBER_CODE_SZ 32
28
+
2429
/*
2530
** SQL code to implement the tables needed by the email notification
2631
** system.
2732
*/
2833
static const char zEmailInit[] =
@@ -42,34 +47,34 @@
4247
@ -- we might also add a separate table that allows subscribing to email
4348
@ -- notifications for specific branches or tags or tickets.
4449
@ --
4550
@ CREATE TABLE repository.subscriber(
4651
@ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use
47
-@ subscriberCode TEXT UNIQUE, -- UUID for subscriber. External use
48
-@ sname TEXT, -- Human readable name
49
-@ suname TEXT, -- Corresponding USER or NULL
50
-@ semail TEXT, -- email address
51
-@ sverify BOOLEAN, -- email address verified
52
+@ subscriberCode BLOB UNIQUE, -- UUID for subscriber. External use
53
+@ semail TEXT UNIQUE COLLATE nocase,-- email address
54
+@ suname TEXT, -- corresponding USER entry
55
+@ sverified BOOLEAN, -- email address verified
5256
@ sdonotcall BOOLEAN, -- true for Do Not Call
5357
@ sdigest BOOLEAN, -- true for daily digests only
5458
@ ssub TEXT, -- baseline subscriptions
5559
@ sctime DATE, -- When this entry was created. JulianDay
5660
@ smtime DATE, -- Last change. JulianDay
57
-@ sipaddr TEXT, -- IP address for last change
58
-@ spswdHash TEXT -- SHA3 hash of password
61
+@ smip TEXT -- IP address of last change
5962
@ );
63
+@ CREATE INDEX repository.subscriberUname
64
+@ ON subscriber(suname) WHERE suname IS NOT NULL;
6065
@
6166
@ -- Email notifications that need to be sent.
6267
@ --
63
-@ -- If the eventid key is an integer, then it corresponds to the
64
-@ -- EVENT.OBJID table. Other kinds of eventids are reserved for
65
-@ -- future expansion.
68
+@ -- The first character of the eventid determines the event type.
69
+@ -- Remaining characters determine the specific event. For example,
70
+@ -- 'c4413' means check-in with rid=4413.
6671
@ --
67
-@ CREATE TABLE repository.email_pending(
68
-@ eventid ANY PRIMARY KEY, -- Object that changed
72
+@ CREATE TABLE repository.pending_alert(
73
+@ eventid TEXT PRIMARY KEY, -- Object that changed
6974
@ sentSep BOOLEAN DEFAULT false, -- individual emails sent
70
-@ sentDigest BOOLEAN DEFAULT false -- digest emails sent
75
+@ sendDigest BOOLEAN DEFAULT false -- digest emails sent
7176
@ ) WITHOUT ROWID;
7277
@
7378
@ -- Record bounced emails. If too many bounces are received within
7479
@ -- some defined time range, then cancel the subscription. Older
7580
@ -- entries are periodically purged.
@@ -85,10 +90,66 @@
8590
** Make sure the unversioned table exists in the repository.
8691
*/
8792
void email_schema(void){
8893
if( !db_table_exists("repository", "subscriber") ){
8994
db_multi_exec(zEmailInit/*works-like:""*/);
95
+ email_triggers_enable();
96
+ }
97
+}
98
+
99
+/*
100
+** Enable triggers that automatically populate the event_pending
101
+** table.
102
+*/
103
+void email_triggers_enable(void){
104
+ if( !db_table_exists("repository","pending_alert") ) return;
105
+ db_multi_exec(
106
+ "CREATE TRIGGER IF NOT EXISTS repository.email_trigger1\n"
107
+ "AFTER INSERT ON event BEGIN\n"
108
+ " INSERT INTO pending_alert(eventid,mtime)\n"
109
+ " SELECT printf('%%.1c%%d',new.type,new.objid),"
110
+ " julianday('now') WHERE true\n"
111
+ " ON CONFLICT(eventId) DO NOTHING;\n"
112
+ "END;"
113
+ );
114
+}
115
+
116
+/*
117
+** Disable triggers the event_pending triggers.
118
+**
119
+** This must be called before rebuilding the EVENT table, for example
120
+** via the "fossil rebuild" command.
121
+*/
122
+void email_triggers_disable(void){
123
+ db_multi_exec(
124
+ "DROP TRIGGER IF EXISTS repository.email_trigger1;\n"
125
+ );
126
+}
127
+
128
+/*
129
+** Return true if email alerts are active.
130
+*/
131
+int email_enabled(void){
132
+ if( !db_table_exists("repository", "subscriber") ) return 0;
133
+ if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0;
134
+ return 1;
135
+}
136
+
137
+
138
+
139
+/*
140
+** Insert a "Subscriber List" submenu link if the current user
141
+** is an administrator.
142
+*/
143
+void email_submenu_common(void){
144
+ if( g.perm.Admin ){
145
+ if( fossil_strcmp(g.zPath,"subscribers") ){
146
+ style_submenu_element("List Subscribers","%R/subscribers");
147
+ }
148
+ if( fossil_strcmp(g.zPath,"subscribe") ){
149
+ style_submenu_element("Add New Subscriber","%R/subscribe");
150
+ }
90151
}
91152
}
92153
93154
94155
/*
@@ -99,64 +160,96 @@
99160
void setup_email(void){
100161
static const char *const azSendMethods[] = {
101162
"off", "Disabled",
102163
"pipe", "Pipe to a command",
103164
"db", "Store in a database",
104
- "file", "Store in a directory"
165
+ "dir", "Store in a directory"
105166
};
106167
login_check_credentials();
107168
if( !g.perm.Setup ){
108169
login_needed(0);
109170
return;
110171
}
111172
db_begin_transaction();
112173
174
+ email_submenu_common();
113175
style_header("Email Notification Setup");
114176
@ <form action="%R/setup_email" method="post"><div>
115177
@ <input type="submit" name="submit" value="Apply Changes" /><hr>
116178
login_insert_csrf_secret();
117
- multiple_choice_attribute("Email Send Method","email-send-method",
118
- "esm", "off", count(azSendMethods)/2, azSendMethods);
179
+
180
+ entry_attribute("Canonical Server URL", 40, "email-url",
181
+ "eurl", "", 0);
182
+ @ <p><b>Required.</b>
183
+ @ This is URL used as the basename for hyperlinks included in
184
+ @ email alert text. Omit the trailing "/".
185
+ @ Suggested value: "%h(g.zBaseURL)"
186
+ @ (Property: "email-url")</p>
187
+ @ <hr>
188
+
189
+ entry_attribute("\"From\" email address", 20, "email-self",
190
+ "eself", "", 0);
191
+ @ <p><b>Required.</b>
192
+ @ This is the email from which email notifications are sent. The
193
+ @ system administrator should arrange for emails sent to this address
194
+ @ to be handed off to the "fossil email incoming" command so that Fossil
195
+ @ can handle bounces. (Property: "email-self")</p>
196
+ @ <hr>
197
+
198
+ entry_attribute("Repository Nickname", 16, "email-subname",
199
+ "enn", "", 0);
200
+ @ <p><b>Required.</b>
201
+ @ This is short name used to identifies the repository in the
202
+ @ Subject: line of email alerts. Traditionally this name is
203
+ @ included in square brackets. Examples: "[fossil-src]", "[sqlite-src]".
204
+ @ (Property: "email-subname")</p>
205
+ @ <hr>
206
+
207
+ multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
208
+ "off", count(azSendMethods)/2, azSendMethods);
119209
@ <p>How to send email. The "Pipe to a command"
120210
@ method is the usual choice in production.
121211
@ (Property: "email-send-method")</p>
122212
@ <hr>
123
- entry_attribute("Command To Pipe Email To", 80, "esc",
124
- "email-send-command", "sendmail -t", 0);
213
+
214
+
215
+ entry_attribute("Command To Pipe Email To", 80, "email-send-command",
216
+ "ecmd", "sendmail -t", 0);
125217
@ <p>When the send method is "pipe to a command", this is the command
126218
@ that is run. Email messages are piped into the standard input of this
127219
@ command. The command is expected to extract the sender address,
128220
@ recepient addresses, and subject from the header of the piped email
129221
@ text. (Property: "email-send-command")</p>
130222
131
- entry_attribute("Database In Which To Store Email", 60, "esdb",
132
- "email-send-db", "", 0);
223
+ entry_attribute("Database In Which To Store Email", 60, "email-send-db",
224
+ "esdb", "", 0);
133225
@ <p>When the send method is "store in a databaes", each email message is
134226
@ stored in an SQLite database file with the name given here.
135227
@ (Property: "email-send-db")</p>
136228
137
- entry_attribute("Directory In Which To Store Email", 60, "esdir",
138
- "email-send-dir", "", 0);
229
+ entry_attribute("Directory In Which To Store Email", 60, "email-send-dir",
230
+ "esdir", "", 0);
139231
@ <p>When the send method is "store in a directory", each email message is
140232
@ stored as a separate file in the directory shown here.
141233
@ (Property: "email-send-dir")</p>
142234
@ <hr>
143235
144
- entry_attribute("\"From\" email address", 40, "ef",
145
- "email-self", "", 0);
146
- @ <p>This is the email from which email notifications are sent. The
147
- @ system administrator should arrange for emails sent to this address
148
- @ to be handed off to the "fossil email incoming" command so that Fossil
149
- @ can handle bounces. (Property: "email-self")</p>
150
- @ <hr>
151
-
152
- entry_attribute("Administrator email address", 40, "ea",
153
- "email-admin", "", 0);
236
+ entry_attribute("Administrator email address", 40, "email-admin",
237
+ "eadmin", "", 0);
154238
@ <p>This is the email for the human administrator for the system.
155239
@ Abuse and trouble reports are send here.
156240
@ (Property: "email-admin")</p>
157241
@ <hr>
242
+
243
+ entry_attribute("Inbound email directory", 40, "email-receive-dir",
244
+ "erdir", "", 0);
245
+ @ <p>Inbound emails can be stored in a directory for analysis as
246
+ @ a debugging aid. Put the name of that directory in this entry box.
247
+ @ Disable saving of inbound email by making this an empty string.
248
+ @ Abuse and trouble reports are send here.
249
+ @ (Property: "email-receive-dir")</p>
250
+ @ <hr>
158251
@ <p><input type="submit" name="submit" value="Apply Changes" /></p>
159252
@ </div></form>
160253
db_end_transaction(0);
161254
style_footer();
162255
}
@@ -172,10 +265,23 @@
172265
k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf);
173266
blob_append(pOut, zBuf, k);
174267
blob_append(pOut, "\r\n", 2);
175268
}
176269
}
270
+
271
+/*
272
+** Come up with a unique filename in the zDir directory.
273
+**
274
+** Space to hold the filename is obtained from mprintf() and must
275
+** be freed using fossil_free() by the caller.
276
+*/
277
+static char *emailTempFilename(const char *zDir){
278
+ char *zFile = db_text(0,
279
+ "SELECT %Q||strftime('/%%Y%%m%%d%%H%%M%%S-','now')||hex(randomblob(8))",
280
+ zDir);
281
+ return zFile;
282
+}
177283
178284
#if defined(_WIN32) || defined(WIN32)
179285
# undef popen
180286
# define popen _popen
181287
# undef pclose
@@ -231,18 +337,20 @@
231337
zBoundary = db_text(0, "SELECT hex(randomblob(20))");
232338
blob_appendf(&all, "Content-Type: multipart/alternative;"
233339
" boundary=\"%s\"\r\n", zBoundary);
234340
}
235341
if( pPlain ){
342
+ blob_add_final_newline(pPlain);
236343
if( zBoundary ){
237344
blob_appendf(&all, "\r\n--%s\r\n", zBoundary);
238345
}
239346
blob_appendf(&all,"Content-Type: text/plain\r\n");
240347
blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
241348
append_base64(&all, pPlain);
242349
}
243350
if( pHtml ){
351
+ blob_add_final_newline(pHtml);
244352
if( zBoundary ){
245353
blob_appendf(&all, "--%s\r\n", zBoundary);
246354
}
247355
blob_appendf(&all,"Content-Type: text/html\r\n");
248356
blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
@@ -281,20 +389,32 @@
281389
fclose(out);
282390
}
283391
}
284392
}else if( strcmp(zDest, "dir")==0 ){
285393
const char *zDir = db_get("email-send-dir","./");
286
- char *zFile = db_text(0,
287
- "SELECT %Q||strftime('/%%Y%%m%%d%%H%%M%%S','now')||hex(randomblob(8))",
288
- zDir);
394
+ char *zFile = emailTempFilename(zDir);
289395
blob_write_to_file(&all, zFile);
290396
fossil_free(zFile);
291397
}else if( strcmp(zDest, "stdout")==0 ){
292398
fossil_print("%s\n", blob_str(&all));
293399
}
294400
blob_zero(&all);
295401
}
402
+
403
+/*
404
+** Analyze and act on a received email.
405
+**
406
+** This routine takes ownership of the Blob parameter and is responsible
407
+** for freeing that blob when it is done with it.
408
+**
409
+** This routine acts on all email messages received from the
410
+** "fossil email inbound" command.
411
+*/
412
+void email_receive(Blob *pMsg){
413
+ /* To Do: Look for bounce messages and possibly disable subscriptions */
414
+ blob_zero(pMsg);
415
+}
296416
297417
/*
298418
** SETTING: email-send-method width=5 default=off
299419
** Determine the method used to send email. Allowed values are
300420
** "off", "pipe", "dir", "db", and "stdout". The "off" value means
@@ -326,19 +446,37 @@
326446
/*
327447
** SETTING: email-self width=40
328448
** This is the email address for the repository. Outbound emails add
329449
** this email address as the "From:" field.
330450
*/
451
+/*
452
+** SETTING: email-receive-dir width=40
453
+** Inbound email messages are saved as separate files in this directory,
454
+** for debugging analysis. Disable saving of inbound emails omitting
455
+** this setting, or making it an empty string.
456
+*/
331457
332458
333459
/*
334460
** COMMAND: email
335461
**
336462
** Usage: %fossil email SUBCOMMAND ARGS...
337463
**
338464
** Subcommands:
339465
**
466
+** exec Compose and send pending email alerts.
467
+** Some installations may want to do this via
468
+** a cron-job to make sure alerts are sent
469
+** in a timely manner.
470
+** Options:
471
+**
472
+** --digest Send digests
473
+**
474
+** inbound [FILE] Receive an inbound email message. This message
475
+** is analyzed to see if it is a bounce, and if
476
+** necessary, subscribers may be disabled.
477
+**
340478
** reset Hard reset of all email notification tables
341479
** in the repository. This erases all subscription
342480
** information. Use with extreme care.
343481
**
344482
** send TO [OPTIONS] Send a single email message using whatever
@@ -351,37 +489,71 @@
351489
** --stdout
352490
** --subject|-S SUBJECT
353491
**
354492
** settings [NAME VALUE] With no arguments, list all email settings.
355493
** Or change the value of a single email setting.
494
+**
495
+** subscribers [PATTERN] List all subscribers matching PATTERN.
496
+**
497
+** unsubscribe EMAIL Remove a single subscriber with the given EMAIL.
356498
*/
357499
void email_cmd(void){
358500
const char *zCmd;
359501
int nCmd;
360502
db_find_and_open_repository(0, 0);
361503
email_schema();
362504
zCmd = g.argc>=3 ? g.argv[2] : "x";
363505
nCmd = (int)strlen(zCmd);
506
+ if( strncmp(zCmd, "exec", nCmd)==0 ){
507
+ u32 eFlags = 0;
508
+ if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST;
509
+ verify_all_options();
510
+ email_send_alerts(eFlags);
511
+ }else
512
+ if( strncmp(zCmd, "inbound", nCmd)==0 ){
513
+ Blob email;
514
+ const char *zInboundDir = db_get("email-receive-dir","");
515
+ verify_all_options();
516
+ if( g.argc!=3 && g.argc!=4 ){
517
+ usage("inbound [FILE]");
518
+ }
519
+ blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE);
520
+ if( zInboundDir[0] ){
521
+ char *zFN = emailTempFilename(zInboundDir);
522
+ blob_write_to_file(&email, zFN);
523
+ fossil_free(zFN);
524
+ }
525
+ email_receive(&email);
526
+ }else
364527
if( strncmp(zCmd, "reset", nCmd)==0 ){
365
- Blob yn;
366528
int c;
367
- fossil_print(
368
- "This will erase all content in the repository tables, thus\n"
369
- "deleting all subscriber information. The information will be\n"
370
- "unrecoverable.\n");
371
- prompt_user("Continue? (y/N) ", &yn);
372
- c = blob_str(&yn)[0];
529
+ int bForce = find_option("force","f",0)!=0;
530
+ verify_all_options();
531
+ if( bForce ){
532
+ c = 'y';
533
+ }else{
534
+ Blob yn;
535
+ fossil_print(
536
+ "This will erase all content in the repository tables, thus\n"
537
+ "deleting all subscriber information. The information will be\n"
538
+ "unrecoverable.\n");
539
+ prompt_user("Continue? (y/N) ", &yn);
540
+ c = blob_str(&yn)[0];
541
+ blob_zero(&yn);
542
+ }
373543
if( c=='y' ){
544
+ email_triggers_disable();
374545
db_multi_exec(
375546
"DROP TABLE IF EXISTS subscriber;\n"
376
- "DROP TABLE IF EXISTS subscription;\n"
547
+ "DROP TABLE IF EXISTS pending_alert;\n"
548
+ "DROP TABLE IF EXISTS email_bounce;\n"
549
+ /* Legacy */
377550
"DROP TABLE IF EXISTS email_pending;\n"
378
- "DROP TABLE IF EXISTS email_bounce;\n"
551
+ "DROP TABLE IF EXISTS subscription;\n"
379552
);
380553
email_schema();
381554
}
382
- blob_zero(&yn);
383555
}else
384556
if( strncmp(zCmd, "send", nCmd)==0 ){
385557
Blob prompt, body, hdr;
386558
int sendAsBoth = find_option("both",0,0)!=0;
387559
int sendAsHtml = find_option("html",0,0)!=0;
@@ -402,10 +574,11 @@
402574
if( zSource ){
403575
blob_read_from_file(&body, zSource, ExtFILE);
404576
}else{
405577
prompt_for_user_comment(&body, &prompt);
406578
}
579
+ blob_add_final_newline(&body);
407580
if( sendAsHtml ){
408581
email_send(&hdr, 0, &body, zDest);
409582
}else if( sendAsBoth ){
410583
Blob html;
411584
blob_init(&html, 0, 0);
@@ -416,14 +589,13 @@
416589
email_send(&hdr, &body, 0, zDest);
417590
}
418591
blob_zero(&hdr);
419592
blob_zero(&body);
420593
blob_zero(&prompt);
421
- }
422
- else if( strncmp(zCmd, "settings", nCmd)==0 ){
594
+ }else
595
+ if( strncmp(zCmd, "settings", nCmd)==0 ){
423596
int isGlobal = find_option("global",0,0)!=0;
424
- int i;
425597
int nSetting;
426598
const Setting *pSetting = setting_info(&nSetting);
427599
db_open_config(1, 0);
428600
verify_all_options();
429601
if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]");
@@ -439,15 +611,133 @@
439611
pSetting = setting_info(&nSetting);
440612
for(; nSetting>0; nSetting--, pSetting++ ){
441613
if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
442614
print_setting(pSetting);
443615
}
616
+ }else
617
+ if( strncmp(zCmd, "subscribers", nCmd)==0 ){
618
+ Stmt q;
619
+ verify_all_options();
620
+ if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]");
621
+ if( g.argc==4 ){
622
+ char *zPattern = g.argv[3];
623
+ db_prepare(&q,
624
+ "SELECT semail FROM subscriber"
625
+ " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'"
626
+ " OR semail GLOB '*%q*' or suname GLOB '*%q*'"
627
+ " ORDER BY semail",
628
+ zPattern, zPattern, zPattern, zPattern);
629
+ }else{
630
+ db_prepare(&q,
631
+ "SELECT semail FROM subscriber"
632
+ " ORDER BY semail");
633
+ }
634
+ while( db_step(&q)==SQLITE_ROW ){
635
+ fossil_print("%s\n", db_column_text(&q, 0));
636
+ }
637
+ db_finalize(&q);
638
+ }else
639
+ if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
640
+ verify_all_options();
641
+ if( g.argc!=4 ) usage("unsubscribe EMAIL");
642
+ db_multi_exec(
643
+ "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
644
+ }else
645
+ {
646
+ usage("exec|inbound|reset|send|setting|subscribers|unsubscribe");
647
+ }
648
+}
649
+
650
+/*
651
+** Do error checking on a submitted subscription form. Return TRUE
652
+** if the submission is valid. Return false if any problems are seen.
653
+*/
654
+static int subscribe_error_check(
655
+ int *peErr, /* Type of error */
656
+ char **pzErr, /* Error message text */
657
+ int needCaptcha /* True if captcha check needed */
658
+){
659
+ const char *zEAddr;
660
+ int i, j, n;
661
+ char c;
662
+
663
+ *peErr = 0;
664
+ *pzErr = 0;
665
+
666
+ /* Check the validity of the email address.
667
+ **
668
+ ** (1) Exactly one '@' character.
669
+ ** (2) No other characters besides [a-zA-Z0-9._-]
670
+ */
671
+ zEAddr = P("e");
672
+ if( zEAddr==0 ) return 0;
673
+ for(i=j=0; (c = zEAddr[i])!=0; i++){
674
+ if( c=='@' ){
675
+ n = i;
676
+ j++;
677
+ continue;
678
+ }
679
+ if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){
680
+ *peErr = 1;
681
+ *pzErr = mprintf("illegal character in email address: 0x%x '%c'",
682
+ c, c);
683
+ return 0;
684
+ }
685
+ }
686
+ if( j!=1 ){
687
+ *peErr = 1;
688
+ *pzErr = mprintf("email address should contain exactly one '@'");
689
+ return 0;
690
+ }
691
+ if( n<1 ){
692
+ *peErr = 1;
693
+ *pzErr = mprintf("name missing before '@' in email address");
694
+ return 0;
695
+ }
696
+ if( n>i-5 ){
697
+ *peErr = 1;
698
+ *pzErr = mprintf("email domain too short");
699
+ return 0;
700
+ }
701
+
702
+ /* Verify the captcha */
703
+ if( needCaptcha && !captcha_is_correct(1) ){
704
+ *peErr = 2;
705
+ *pzErr = mprintf("incorrect security code");
706
+ return 0;
444707
}
445
- else{
446
- usage("reset|send|setting");
708
+
709
+ /* Check to make sure the email address is available for reuse */
710
+ if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
711
+ *peErr = 1;
712
+ *pzErr = mprintf("this email address is used by someone else");
713
+ return 0;
447714
}
715
+
716
+ /* If we reach this point, all is well */
717
+ return 1;
448718
}
719
+
720
+/*
721
+** Text of email message sent in order to confirm a subscription.
722
+*/
723
+static const char zConfirmMsg[] =
724
+@ Someone has signed you up for email alerts on the Fossil repository
725
+@ at %s.
726
+@
727
+@ To confirm your subscription and begin receiving alerts, click on
728
+@ the following hyperlink:
729
+@
730
+@ %s/alerts/%s
731
+@
732
+@ Save the hyperlink above! You can reuse this same hyperlink to
733
+@ unsubscribe or to change the kinds of alerts you receive.
734
+@
735
+@ If you do not want to subscribe, you can simply ignore this message.
736
+@ You will not be contacted again.
737
+@
738
+;
449739
450740
/*
451741
** WEBPAGE: subscribe
452742
**
453743
** Allow users to subscribe to email notifications, or to change or
@@ -455,65 +745,162 @@
455745
*/
456746
void subscribe_page(void){
457747
int needCaptcha;
458748
unsigned int uSeed;
459749
const char *zDecoded;
460
- char *zCaptcha;
750
+ char *zCaptcha = 0;
751
+ char *zErr = 0;
752
+ int eErr = 0;
461753
462754
login_check_credentials();
463755
if( !g.perm.EmailAlert ){
464756
login_needed(g.anon.EmailAlert);
465757
return;
466758
}
467
- style_header("Email Subscription");
468
- needCaptcha = P("usecaptcha")!=0 || login_is_nobody()
469
- || login_is_special(g.zLogin);
759
+ if( login_is_individual()
760
+ && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin)
761
+ ){
762
+ /* This person is already signed up for email alerts. Jump
763
+ ** to the screen that lets them edit their alert preferences.
764
+ ** Except, administrators can create subscriptions for others so
765
+ ** do not jump for them.
766
+ */
767
+ if( g.perm.Admin ){
768
+ /* Admins get a link to admin their own account, but they
769
+ ** stay on this page so that they can create subscriptions
770
+ ** for other people. */
771
+ style_submenu_element("My Subscription","%R/alerts");
772
+ }else{
773
+ /* Everybody else jumps to the page to administer their own
774
+ ** account only. */
775
+ cgi_redirectf("%R/alerts");
776
+ return;
777
+ }
778
+ }
779
+ email_submenu_common();
780
+ needCaptcha = !login_is_individual();
781
+ if( P("submit")
782
+ && cgi_csrf_safe(1)
783
+ && subscribe_error_check(&eErr,&zErr,needCaptcha)
784
+ ){
785
+ /* A validated request for a new subscription has been received. */
786
+ char ssub[20];
787
+ const char *zEAddr = P("e");
788
+ sqlite3_int64 id; /* New subscriber Id */
789
+ const char *zCode; /* New subscriber code (in hex) */
790
+ int nsub = 0;
791
+ const char *suname = PT("suname");
792
+ if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin;
793
+ if( suname && suname[0]==0 ) suname = 0;
794
+ if( PB("sa") ) ssub[nsub++] = 'a';
795
+ if( PB("sc") ) ssub[nsub++] = 'c';
796
+ if( PB("st") ) ssub[nsub++] = 't';
797
+ if( PB("sw") ) ssub[nsub++] = 'w';
798
+ ssub[nsub] = 0;
799
+ db_multi_exec(
800
+ "INSERT INTO subscriber(subscriberCode,semail,suname,"
801
+ " sverified,sdonotcall,sdigest,ssub,sctime,smtime,smip)"
802
+ "VALUES(randomblob(32),%Q,%Q,%d,0,%d,%Q,"
803
+ " julianday('now'),julianday('now'),%Q)",
804
+ /* semail */ zEAddr,
805
+ /* suname */ suname,
806
+ /* sverified */ needCaptcha==0,
807
+ /* sdigest */ PB("di"),
808
+ /* ssub */ ssub,
809
+ /* smip */ g.zIpAddr
810
+ );
811
+ id = db_last_insert_rowid();
812
+ zCode = db_text(0,
813
+ "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld",
814
+ id);
815
+ if( !needCaptcha ){
816
+ /* The new subscription has been added on behalf of a logged-in user.
817
+ ** No verification is required. Jump immediately to /alerts page.
818
+ */
819
+ cgi_redirectf("%R/alerts/%s", zCode);
820
+ return;
821
+ }else{
822
+ /* We need to send a verification email */
823
+ Blob hdr, body;
824
+ blob_init(&hdr,0,0);
825
+ blob_init(&body,0,0);
826
+ blob_appendf(&hdr, "To: %s\n", zEAddr);
827
+ blob_appendf(&hdr, "Subject: Subscription verification\n");
828
+ blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/,
829
+ g.zBaseURL, g.zBaseURL, zCode);
830
+ email_send(&hdr, &body, 0, 0);
831
+ style_header("Email Alert Verification");
832
+ @ <p>An email has been sent to "%h(zEAddr)". That email contains a
833
+ @ hyperlink that you must click on in order to activate your
834
+ @ subscription.</p>
835
+ style_footer();
836
+ }
837
+ return;
838
+ }
839
+ style_header("Signup For Email Alerts");
840
+ @ <p>To receive email notifications for changes to this
841
+ @ repository, fill out the form below and press "Submit" button.</p>
470842
form_begin(0, "%R/subscribe");
471843
@ <table class="subscribe">
472844
@ <tr>
473845
@ <td class="form_label">Email&nbsp;Address:</td>
474
- @ <td><input type="text" name="e" value="" size="30"></td>
475
- @ <td></td>
846
+ @ <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td>
847
+ if( eErr==1 ){
848
+ @ <td><span class="loginError">&larr; %h(zErr)</span></td>
849
+ }
476850
@ </tr>
477851
if( needCaptcha ){
478852
uSeed = captcha_seed();
479853
zDecoded = captcha_decode(uSeed);
480854
zCaptcha = captcha_render(zDecoded);
481855
@ <tr>
482856
@ <td class="form_label">Security Code:</td>
483857
@ <td><input type="text" name="captcha" value="" size="30">
484
- @ <input type="hidden" name="usecaptcha" value="1"></td>
485
- @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
486
- @ <td><span class="optionalTag">(copy from below)</span></td>
487
- @ </tr>
488
- }
489
- @ <tr>
490
- @ <td class="form_label">Nickname:</td>
491
- @ <td><input type="text" name="nn" value="" size="30"></td>
492
- @ <td><span class="optionalTag">(optional)</span></td>
493
- @ </tr>
494
- @ <tr>
495
- @ <td class="form_label">Password:</td>
496
- @ <td><input type="password" name="pw" value="" size="30"></td>
497
- @ <td><span class="optionalTag">(optional)</span></td>
498
- @ </tr>
499
- @ <tr>
500
- @ <td class="form_label">Options:</td>
501
- @ <td><label><input type="checkbox" name="sa" value="0">\
502
- @ Announcements</label><br>
503
- @ <label><input type="checkbox" name="sc" value="0">\
504
- @ Check-ins</label><br>
505
- @ <label><input type="checkbox" name="st" value="0">\
506
- @ Ticket changes</label><br>
507
- @ <label><input type="checkbox" name="sw" value="0">\
508
- @ Wiki</label><br>
509
- @ <label><input type="checkbox" name="di" value="0">\
510
- @ Daily digest only</label><br></td>
858
+ @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
859
+ if( eErr==2 ){
860
+ @ <td><span class="loginError">&larr; %h(zErr)</span></td>
861
+ }
862
+ @ </tr>
863
+ }
864
+ if( g.perm.Admin ){
865
+ @ <tr>
866
+ @ <td class="form_label">User:</td>
867
+ @ <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \
868
+ @ size="30"></td>
869
+ if( eErr==3 ){
870
+ @ <td><span class="loginError">&larr; %h(zErr)</span></td>
871
+ }
872
+ @ </tr>
873
+ }
874
+ @ <tr>
875
+ @ <td class="form_label">Options:</td>
876
+ @ <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \
877
+ @ Announcements</label><br>
878
+ @ <label><input type="checkbox" name="sc" %s(PCK("sc"))> \
879
+ @ Check-ins</label><br>
880
+ @ <label><input type="checkbox" name="st" %s(PCK("st"))> \
881
+ @ Ticket changes</label><br>
882
+ @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
883
+ @ Wiki</label><br>
884
+ @ <label><input type="checkbox" name="di" %s(PCK("di"))> \
885
+ @ Daily digest only</label><br>
886
+ if( g.perm.Admin ){
887
+ @ <label><input type="checkbox" name="vi" %s(PCK("vi"))> \
888
+ @ Verified</label><br>
889
+ @ <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \
890
+ @ Do not call</label><br>
891
+ }
892
+ @ </td>
511893
@ </tr>
512894
@ <tr>
513895
@ <td></td>
514
- @ <td><input type="submit" value="Submit"></td>
896
+ if( needCaptcha && !email_enabled() ){
897
+ @ <td><input type="submit" name="submit" value="Submit" disabled>
898
+ @ (Email current disabled)</td>
899
+ }else{
900
+ @ <td><input type="submit" name="submit" value="Submit"></td>
901
+ }
515902
@ </tr>
516903
@ </table>
517904
if( needCaptcha ){
518905
@ <div class="captcha"><table class="captcha"><tr><td><pre>
519906
@ %h(zCaptcha)
@@ -520,7 +907,701 @@
520907
@ </pre>
521908
@ Enter the 8 characters above in the "Security Code" box
522909
@ </td></tr></table></div>
523910
}
524911
@ </form>
912
+ fossil_free(zErr);
913
+ style_footer();
914
+}
915
+
916
+/*
917
+** Either shutdown or completely delete a subscription entry given
918
+** by the hex value zName. Then paint a webpage that explains that
919
+** the entry has been removed.
920
+*/
921
+static void email_unsubscribe(const char *zName){
922
+ char *zEmail;
923
+ zEmail = db_text(0, "SELECT semail FROM subscriber"
924
+ " WHERE subscriberCode=hextoblob(%Q)", zName);
925
+ if( zEmail==0 ){
926
+ style_header("Unsubscribe Fail");
927
+ @ <p>Unable to locate a subscriber with the requested key</p>
928
+ }else{
929
+ db_multi_exec(
930
+ "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
931
+ zName
932
+ );
933
+ style_header("Unsubscribed");
934
+ @ <p>The "%h(zEmail)" email address has been delisted.
935
+ @ All traces of that email address have been removed</p>
936
+ }
937
+ style_footer();
938
+ return;
939
+}
940
+
941
+/*
942
+** WEBPAGE: alerts
943
+**
944
+** Edit email alert and notification settings.
945
+**
946
+** The subscriber entry is identified in either of two ways:
947
+**
948
+** (1) The name= query parameter contains the subscriberCode.
949
+**
950
+** (2) The user is logged into an account other than "nobody" or
951
+** "anonymous". In that case the notification settings
952
+** associated with that account can be edited without needing
953
+** to know the subscriber code.
954
+*/
955
+void alerts_page(void){
956
+ const char *zName = P("name");
957
+ Stmt q;
958
+ int sa, sc, st, sw;
959
+ int sdigest, sdonotcall, sverified;
960
+ const char *ssub;
961
+ const char *semail;
962
+ const char *smip;
963
+ const char *suname;
964
+ int eErr = 0;
965
+ char *zErr = 0;
966
+
967
+ login_check_credentials();
968
+ if( !g.perm.EmailAlert ){
969
+ cgi_redirect("subscribe");
970
+ return;
971
+ }
972
+ if( zName==0 && login_is_individual() ){
973
+ zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
974
+ " WHERE suname=%Q", g.zLogin);
975
+ }
976
+ if( zName==0 || !validate16(zName, -1) ){
977
+ cgi_redirect("subscribe");
978
+ return;
979
+ }
980
+ email_submenu_common();
981
+ if( P("submit")!=0 && cgi_csrf_safe(1) ){
982
+ int sdonotcall = PB("sdonotcall");
983
+ int sdigest = PB("sdigest");
984
+ char ssub[10];
985
+ int nsub = 0;
986
+ if( PB("sa") ) ssub[nsub++] = 'a';
987
+ if( PB("sc") ) ssub[nsub++] = 'c';
988
+ if( PB("st") ) ssub[nsub++] = 't';
989
+ if( PB("sw") ) ssub[nsub++] = 'w';
990
+ ssub[nsub] = 0;
991
+ if( g.perm.Admin ){
992
+ const char *suname = PT("suname");
993
+ if( suname && suname[0]==0 ) suname = 0;
994
+ int sverified = PB("sverified");
995
+ db_multi_exec(
996
+ "UPDATE subscriber SET"
997
+ " sdonotcall=%d,"
998
+ " sdigest=%d,"
999
+ " ssub=%Q,"
1000
+ " smtime=julianday('now'),"
1001
+ " smip=%Q,"
1002
+ " suname=%Q,"
1003
+ " sverified=%d"
1004
+ " WHERE subscriberCode=hextoblob(%Q)",
1005
+ sdonotcall,
1006
+ sdigest,
1007
+ ssub,
1008
+ g.zIpAddr,
1009
+ suname,
1010
+ sverified,
1011
+ zName
1012
+ );
1013
+ }else{
1014
+ db_multi_exec(
1015
+ "UPDATE subscriber SET"
1016
+ " sdonotcall=%d,"
1017
+ " sdigest=%d,"
1018
+ " ssub=%Q,"
1019
+ " smtime=julianday('now'),"
1020
+ " smip=%Q,"
1021
+ " WHERE subscriberCode=hextoblob(%Q)",
1022
+ sdonotcall,
1023
+ sdigest,
1024
+ ssub,
1025
+ g.zIpAddr,
1026
+ zName
1027
+ );
1028
+ }
1029
+ }
1030
+ if( P("delete")!=0 && cgi_csrf_safe(1) ){
1031
+ if( !PB("dodelete") ){
1032
+ eErr = 9;
1033
+ zErr = mprintf("Select this checkbox and press \"Unsubscribe\" to"
1034
+ " unsubscribe");
1035
+ }else{
1036
+ email_unsubscribe(zName);
1037
+ return;
1038
+ }
1039
+ }
1040
+ db_prepare(&q,
1041
+ "SELECT"
1042
+ " semail,"
1043
+ " sverified,"
1044
+ " sdonotcall,"
1045
+ " sdigest,"
1046
+ " ssub,"
1047
+ " smip,"
1048
+ " suname"
1049
+ " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
1050
+ if( db_step(&q)!=SQLITE_ROW ){
1051
+ db_finalize(&q);
1052
+ cgi_redirect("subscribe");
1053
+ return;
1054
+ }
1055
+ style_header("Update Subscription");
1056
+ semail = db_column_text(&q, 0);
1057
+ sverified = db_column_int(&q, 1);
1058
+ sdonotcall = db_column_int(&q, 2);
1059
+ sdigest = db_column_int(&q, 3);
1060
+ ssub = db_column_text(&q, 4);
1061
+ sa = strchr(ssub,'a')!=0;
1062
+ sc = strchr(ssub,'c')!=0;
1063
+ st = strchr(ssub,'t')!=0;
1064
+ sw = strchr(ssub,'w')!=0;
1065
+ smip = db_column_text(&q, 5);
1066
+ suname = db_column_text(&q, 6);
1067
+ if( !g.perm.Admin && !sverified ){
1068
+ db_multi_exec(
1069
+ "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
1070
+ zName);
1071
+ @ <h1>Your email alert subscription has been verified!</h1>
1072
+ @ <p>Use the form below to update your subscription information.</p>
1073
+ @ <p>Hint: Bookmark this page so that you can more easily update
1074
+ @ your subscription information in the future</p>
1075
+ }else{
1076
+ @ <p>Make changes to the email subscription shown below and
1077
+ @ press "Submit".</p>
1078
+ }
1079
+ form_begin(0, "%R/alerts");
1080
+ @ <input type="hidden" name="name" value="%h(zName)">
1081
+ @ <table class="subscribe">
1082
+ @ <tr>
1083
+ @ <td class="form_label">Email&nbsp;Address:</td>
1084
+ @ <td>%h(semail)</td>
1085
+ @ </tr>
1086
+ if( g.perm.Admin ){
1087
+ @ <tr>
1088
+ @ <td class='form_label'>IP Address:</td>
1089
+ @ <td>%h(smip)</td>
1090
+ @ </tr>
1091
+ @ <tr>
1092
+ @ <td class="form_label">User:</td>
1093
+ @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1094
+ @ size="30"></td>
1095
+ @ </tr>
1096
+ }
1097
+ @ <tr>
1098
+ @ <td class="form_label">Options:</td>
1099
+ @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
1100
+ @ Announcements</label><br>
1101
+ @ <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\
1102
+ @ Check-ins</label><br>
1103
+ @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
1104
+ @ Ticket changes</label><br>
1105
+ @ <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\
1106
+ @ Wiki</label><br>
1107
+ @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
1108
+ @ Daily digest only</label><br>
1109
+ if( g.perm.Admin ){
1110
+ @ <label><input type="checkbox" name="sdonotcall" \
1111
+ @ %s(sdonotcall?"checked":"")> Do not call</label><br>
1112
+ @ <label><input type="checkbox" name="sverified" \
1113
+ @ %s(sverified?"checked":"")>\
1114
+ @ Verified</label><br>
1115
+ }
1116
+ @ <label><input type="checkbox" name="dodelete">
1117
+ @ Unsubscribe</label> \
1118
+ if( eErr==9 ){
1119
+ @ <span class="loginError">&larr; %h(zErr)</span>\
1120
+ }
1121
+ @ <br>
1122
+ @ </td></tr>
1123
+ @ <tr>
1124
+ @ <td></td>
1125
+ @ <td><input type="submit" name="submit" value="Submit">
1126
+ @ <input type="submit" name="delete" value="Unsubscribe">
1127
+ @ </tr>
1128
+ @ </table>
1129
+ @ </form>
1130
+ fossil_free(zErr);
1131
+ db_finalize(&q);
1132
+ style_footer();
1133
+}
1134
+
1135
+/* This is the message that gets sent to describe how to change
1136
+** or modify a subscription
1137
+*/
1138
+static const char zUnsubMsg[] =
1139
+@ To changes your subscription settings at %s visit this link:
1140
+@
1141
+@ %s/alerts/%s
1142
+@
1143
+@ To completely unsubscribe from %s, visit the following link:
1144
+@
1145
+@ %s/unsubscribe/%s
1146
+;
1147
+
1148
+/*
1149
+** WEBPAGE: unsubscribe
1150
+**
1151
+** Users visit this page to be delisted from email alerts.
1152
+**
1153
+** If a valid subscriber code is supplied in the name= query parameter,
1154
+** then that subscriber is delisted.
1155
+**
1156
+** Otherwise, If the users is logged in, then they are redirected
1157
+** to the /alerts page where they have an unsubscribe button.
1158
+**
1159
+** Non-logged-in users with no name= query parameter are invited to enter
1160
+** an email address to which will be sent the unsubscribe link that
1161
+** contains the correct subscriber code.
1162
+*/
1163
+void unsubscribe_page(void){
1164
+ const char *zName = P("name");
1165
+ char *zErr = 0;
1166
+ int eErr = 0;
1167
+ unsigned int uSeed;
1168
+ const char *zDecoded;
1169
+ char *zCaptcha = 0;
1170
+ int dx;
1171
+ int bSubmit;
1172
+ const char *zEAddr;
1173
+ char *zCode = 0;
1174
+
1175
+ /* If a valid subscriber code is supplied, then unsubscribe immediately.
1176
+ */
1177
+ if( zName
1178
+ && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1179
+ zName)
1180
+ ){
1181
+ email_unsubscribe(zName);
1182
+ return;
1183
+ }
1184
+
1185
+ /* Logged in users are redirected to the /alerts page */
1186
+ login_check_credentials();
1187
+ if( login_is_individual() ){
1188
+ cgi_redirectf("%R/alerts");
1189
+ return;
1190
+ }
1191
+
1192
+ zEAddr = PD("e","");
1193
+ dx = atoi(PD("dx","0"));
1194
+ bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(1);
1195
+ if( bSubmit ){
1196
+ if( !captcha_is_correct(1) ){
1197
+ eErr = 2;
1198
+ zErr = mprintf("enter the security code shown below");
1199
+ bSubmit = 0;
1200
+ }
1201
+ }
1202
+ if( bSubmit ){
1203
+ zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber"
1204
+ " WHERE semail=%Q", zEAddr);
1205
+ if( zCode==0 ){
1206
+ eErr = 1;
1207
+ zErr = mprintf("not a valid email address");
1208
+ bSubmit = 0;
1209
+ }
1210
+ }
1211
+ if( bSubmit ){
1212
+ /* If we get this far, it means that a valid unsubscribe request has
1213
+ ** been submitted. Send the appropriate email. */
1214
+ Blob hdr, body;
1215
+ blob_init(&hdr,0,0);
1216
+ blob_init(&body,0,0);
1217
+ blob_appendf(&hdr, "To: %s\n", zEAddr);
1218
+ blob_appendf(&hdr, "Subject: Unsubscribe Instructions\n");
1219
+ blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
1220
+ g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
1221
+ email_send(&hdr, &body, 0, 0);
1222
+ style_header("Unsubscribe Instructions Sent");
1223
+ @ <p>An email has been sent to "%h(zEAddr)" that explains how to
1224
+ @ unsubscribe and/or modify your subscription settings</p>
1225
+ style_footer();
1226
+ return;
1227
+ }
1228
+
1229
+ /* Non-logged-in users have to enter an email address to which is
1230
+ ** sent a message containing the unsubscribe link.
1231
+ */
1232
+ style_header("Unsubscribe Request");
1233
+ @ <p>Fill out the form below to request an email message that will
1234
+ @ explain how to unsubscribe and/or change your subscription settings.</p>
1235
+ @
1236
+ form_begin(0, "%R/unsubscribe");
1237
+ @ <table class="subscribe">
1238
+ @ <tr>
1239
+ @ <td class="form_label">Email&nbsp;Address:</td>
1240
+ @ <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td>
1241
+ if( eErr==1 ){
1242
+ @ <td><span class="loginError">&larr; %h(zErr)</span></td>
1243
+ }
1244
+ @ </tr>
1245
+ uSeed = captcha_seed();
1246
+ zDecoded = captcha_decode(uSeed);
1247
+ zCaptcha = captcha_render(zDecoded);
1248
+ @ <tr>
1249
+ @ <td class="form_label">Security Code:</td>
1250
+ @ <td><input type="text" name="captcha" value="" size="30">
1251
+ @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1252
+ if( eErr==2 ){
1253
+ @ <td><span class="loginError">&larr; %h(zErr)</span></td>
1254
+ }
1255
+ @ </tr>
1256
+ @ <tr>
1257
+ @ <td class="form_label">Options:</td>
1258
+ @ <td><label><input type="radio" name="dx" value="0" %s(dx?"":"checked")>\
1259
+ @ Modify subscription</label><br>
1260
+ @ <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\
1261
+ @ Completely unsubscribe</label><br>
1262
+ @ <tr>
1263
+ @ <td></td>
1264
+ @ <td><input type="submit" name="submit" value="Submit"></td>
1265
+ @ </tr>
1266
+ @ </table>
1267
+ @ <div class="captcha"><table class="captcha"><tr><td><pre>
1268
+ @ %h(zCaptcha)
1269
+ @ </pre>
1270
+ @ Enter the 8 characters above in the "Security Code" box
1271
+ @ </td></tr></table></div>
1272
+ @ </form>
1273
+ fossil_free(zErr);
1274
+ style_footer();
1275
+}
1276
+
1277
+/*
1278
+** WEBPAGE: subscribers
1279
+**
1280
+** This page, accessible to administrators only,
1281
+** shows a list of email notification email addresses with
1282
+** links to facilities for editing.
1283
+*/
1284
+void subscriber_list_page(void){
1285
+ Blob sql;
1286
+ Stmt q;
1287
+ login_check_credentials();
1288
+ if( !g.perm.Admin ){
1289
+ fossil_redirect_home();
1290
+ return;
1291
+ }
1292
+ email_submenu_common();
1293
+ style_header("Subscriber List");
1294
+ blob_init(&sql, 0, 0);
1295
+ blob_append_sql(&sql,
1296
+ "SELECT hex(subscriberCode),"
1297
+ " semail,"
1298
+ " ssub,"
1299
+ " suname,"
1300
+ " sverified,"
1301
+ " sdigest"
1302
+ " FROM subscriber"
1303
+ );
1304
+ db_prepare_blob(&q, &sql);
1305
+ @ <table border="1">
1306
+ @ <tr>
1307
+ @ <th>Email
1308
+ @ <th>Events
1309
+ @ <th>Digest-Only?
1310
+ @ <th>User
1311
+ @ <th>Verified?
1312
+ @ </tr>
1313
+ while( db_step(&q)==SQLITE_ROW ){
1314
+ @ <tr>
1315
+ @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
1316
+ @ %h(db_column_text(&q,1))</a></td>
1317
+ @ <td>%h(db_column_text(&q,2))</td>
1318
+ @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
1319
+ @ <td>%h(db_column_text(&q,3))</td>
1320
+ @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
1321
+ @ </tr>
1322
+ }
1323
+ @ </table>
1324
+ db_finalize(&q);
5251325
style_footer();
5261326
}
1327
+
1328
+#if LOCAL_INTERFACE
1329
+/*
1330
+** A single event that might appear in an alert is recorded as an
1331
+** instance of the following object.
1332
+*/
1333
+struct EmailEvent {
1334
+ int type; /* 'c', 't', 'w', etc. */
1335
+ Blob txt; /* Text description to appear in an alert */
1336
+ EmailEvent *pNext; /* Next in chronological order */
1337
+};
1338
+#endif
1339
+
1340
+/*
1341
+** Free a linked list of EmailEvent objects
1342
+*/
1343
+void email_free_eventlist(EmailEvent *p){
1344
+ while( p ){
1345
+ EmailEvent *pNext = p->pNext;
1346
+ blob_zero(&p->txt);
1347
+ fossil_free(p);
1348
+ p = pNext;
1349
+ }
1350
+}
1351
+
1352
+/*
1353
+** Compute and return a linked list of EmailEvent objects
1354
+** corresponding to the current content of the temp.wantalert
1355
+** table which should be defined as follows:
1356
+**
1357
+** CREATE TEMP TABLE wantalert(eventId TEXT);
1358
+*/
1359
+EmailEvent *email_compute_event_text(int *pnEvent){
1360
+ Stmt q;
1361
+ EmailEvent *p;
1362
+ EmailEvent anchor;
1363
+ EmailEvent *pLast;
1364
+ const char *zUrl = db_get("email-url","http://localhost:8080");
1365
+
1366
+ db_prepare(&q,
1367
+ "SELECT"
1368
+ " blob.uuid," /* 0 */
1369
+ " datetime(event.mtime)," /* 1 */
1370
+ " coalesce(ecomment,comment)"
1371
+ " || ' (user: ' || coalesce(euser,user,'?')"
1372
+ " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
1373
+ " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
1374
+ " FROM tag, tagxref"
1375
+ " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
1376
+ " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))"
1377
+ " || ')' as comment," /* 2 */
1378
+ " tagxref.value AS branch," /* 3 */
1379
+ " wantalert.eventId" /* 4 */
1380
+ " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob"
1381
+ " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid"
1382
+ " AND tagxref.tagtype>0"
1383
+ " AND tagxref.rid=blob.rid"
1384
+ " WHERE blob.rid=event.objid"
1385
+ " AND tag.tagname='branch'"
1386
+ " AND event.objid=substr(wantalert.eventId,2)+0"
1387
+ " ORDER BY event.mtime"
1388
+ );
1389
+ memset(&anchor, 0, sizeof(anchor));
1390
+ pLast = &anchor;
1391
+ *pnEvent = 0;
1392
+ while( db_step(&q)==SQLITE_ROW ){
1393
+ const char *zType = "";
1394
+ p = fossil_malloc( sizeof(EmailEvent) );
1395
+ pLast->pNext = p;
1396
+ pLast = p;
1397
+ p->type = db_column_text(&q, 4)[0];
1398
+ p->pNext = 0;
1399
+ switch( p->type ){
1400
+ case 'c': zType = "Check-In"; break;
1401
+ case 't': zType = "Wiki Edit"; break;
1402
+ case 'w': zType = "Ticket Change"; break;
1403
+ }
1404
+ blob_init(&p->txt, 0, 0);
1405
+ blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
1406
+ db_column_text(&q,1),
1407
+ zType,
1408
+ db_column_text(&q,2),
1409
+ zUrl,
1410
+ db_column_text(&q,0)
1411
+ );
1412
+ (*pnEvent)++;
1413
+ }
1414
+ db_finalize(&q);
1415
+ return anchor.pNext;
1416
+}
1417
+
1418
+/*
1419
+** Put a header on an alert email
1420
+*/
1421
+void email_header(Blob *pOut){
1422
+ blob_appendf(pOut,
1423
+ "This is an automated email reporting changes "
1424
+ "on Fossil repository %s (%s/timeline)\n",
1425
+ db_get("email-subname","(unknown)"),
1426
+ db_get("email-url","http://localhost:8080"));
1427
+}
1428
+
1429
+/*
1430
+** Append the "unsubscribe" notification and other footer text to
1431
+** the end of an email alert being assemblied in pOut.
1432
+*/
1433
+void email_footer(Blob *pOut){
1434
+ blob_appendf(pOut, "\n%.72c\nTo unsubscribe: %s/unsubscribe\n",
1435
+ '-', db_get("email-url","http://localhost:8080"));
1436
+}
1437
+
1438
+/*
1439
+** COMMAND: test-generate-alert
1440
+**
1441
+** Usage: %fossil test-generate-alert [--html] [--actual] EVENTID ...
1442
+**
1443
+** Generate the text of an email alert for all of the EVENTIDs
1444
+** listed on the command-line. Write that text to standard
1445
+** output. If the --actual flag is present, then the EVENTIDs are
1446
+** the actual event-ids in the pending_alert table.
1447
+**
1448
+** This command is intended for testing and debugging the logic
1449
+** that generates email alert text.
1450
+*/
1451
+void test_generate_alert_cmd(void){
1452
+ int bActual = find_option("actual",0,0)!=0;
1453
+ Blob out;
1454
+ int nEvent;
1455
+ EmailEvent *pEvent, *p;
1456
+
1457
+ db_find_and_open_repository(0, 0);
1458
+ verify_all_options();
1459
+ db_begin_transaction();
1460
+ email_schema();
1461
+ db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)");
1462
+ if( bActual ){
1463
+ db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert");
1464
+ }else{
1465
+ int i;
1466
+ for(i=2; i<g.argc; i++){
1467
+ db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]);
1468
+ }
1469
+ }
1470
+ blob_init(&out, 0, 0);
1471
+ email_header(&out);
1472
+ pEvent = email_compute_event_text(&nEvent);
1473
+ for(p=pEvent; p; p=p->pNext){
1474
+ blob_append(&out, "\n", 1);
1475
+ blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
1476
+ }
1477
+ email_free_eventlist(pEvent);
1478
+ email_footer(&out);
1479
+ fossil_print("%s", blob_str(&out));
1480
+ blob_zero(&out);
1481
+ db_end_transaction(0);
1482
+}
1483
+
1484
+/*
1485
+** COMMAND: test-add-alerts
1486
+**
1487
+** Usage: %fossil test-add-alerts EVENTID ...
1488
+**
1489
+** Add one or more events to the pending_alert queue. Use this
1490
+** command during testing to force email notifications for specific
1491
+** events.
1492
+*/
1493
+void test_add_alert_cmd(void){
1494
+ int i;
1495
+ db_find_and_open_repository(0, 0);
1496
+ verify_all_options();
1497
+ db_begin_transaction();
1498
+ email_schema();
1499
+ for(i=2; i<g.argc; i++){
1500
+ db_multi_exec("INSERT INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]);
1501
+ }
1502
+ db_end_transaction(0);
1503
+}
1504
+
1505
+#if INTERFACE
1506
+/*
1507
+** Flags for email_send_alerts()
1508
+*/
1509
+#define SENDALERT_DIGEST 0x0001 /* Send a digest */
1510
+#define SENDALERT_PRESERVE 0x0002 /* Do not mark the task as done */
1511
+
1512
+#endif /* INTERFACE */
1513
+
1514
+/*
1515
+** Send alert emails to all subscribers
1516
+*/
1517
+void email_send_alerts(u32 flags){
1518
+ EmailEvent *pEvents, *p;
1519
+ int nEvent = 0;
1520
+ Stmt q;
1521
+ const char *zDigest = "false";
1522
+ Blob hdr, body;
1523
+ const char *zUrl;
1524
+ const char *zRepoName;
1525
+ const char *zFrom;
1526
+
1527
+ db_begin_transaction();
1528
+ if( !email_enabled() ) goto send_alerts_done;
1529
+ zUrl = db_get("email-url",0);
1530
+ if( zUrl==0 ) goto send_alerts_done;
1531
+ zRepoName = db_get("email-subname",0);
1532
+ if( zRepoName==0 ) goto send_alerts_done;
1533
+ zFrom = db_get("email-self",0);
1534
+ if( zFrom==0 ) goto send_alerts_done;
1535
+ db_multi_exec(
1536
+ "DROP TABLE IF EXISTS temp.wantalert;"
1537
+ "CREATE TEMP TABLE wantalert(eventId TEXT);"
1538
+ );
1539
+ if( flags & SENDALERT_DIGEST ){
1540
+ db_multi_exec(
1541
+ "INSERT INTO wantalert SELECT eventid FROM pending_alert"
1542
+ " WHERE sentDigest IS FALSE"
1543
+ );
1544
+ zDigest = "true";
1545
+ }else{
1546
+ db_multi_exec(
1547
+ "INSERT INTO wantalert SELECT eventid FROM pending_alert"
1548
+ " WHERE sentSep IS FALSE"
1549
+ );
1550
+ }
1551
+ pEvents = email_compute_event_text(&nEvent);
1552
+ if( nEvent==0 ) return;
1553
+ blob_init(&hdr, 0, 0);
1554
+ blob_init(&body, 0, 0);
1555
+ db_prepare(&q,
1556
+ "SELECT"
1557
+ " subscriberCode," /* 0 */
1558
+ " semail," /* 1 */
1559
+ " ssub" /* 2 */
1560
+ " FROM subscriber"
1561
+ " WHERE sverified AND NOT sdonotcall"
1562
+ " AND sdigest IS %s",
1563
+ zDigest/*safe-for-%s*/
1564
+ );
1565
+ while( db_step(&q)==SQLITE_ROW ){
1566
+ const char *zCode = db_column_text(&q, 0);
1567
+ const char *zSub = db_column_text(&q, 2);
1568
+ const char *zEmail = db_column_text(&q, 1);
1569
+ int nHit = 0;
1570
+ for(p=pEvents; p; p=p->pNext){
1571
+ if( strchr(zSub,p->type)==0 ) continue;
1572
+ if( nHit==0 ){
1573
+ blob_appendf(&hdr,"To: %s\n", zEmail);
1574
+ blob_appendf(&hdr,"From: %s\n", zFrom);
1575
+ blob_appendf(&hdr,"Subject: %s activity alert\n", zRepoName);
1576
+ blob_appendf(&body,
1577
+ "This is an automated email sent by the Fossil repository "
1578
+ "at %s to alert you to changes.\n",
1579
+ zUrl
1580
+ );
1581
+ }
1582
+ nHit++;
1583
+ blob_append(&body, "\n", 1);
1584
+ blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
1585
+ }
1586
+ if( nHit==0 ) continue;
1587
+ blob_appendf(&body,"\n%.72c\nSubscription info: %s/alerts/%s\n",
1588
+ '-', zUrl, zCode);
1589
+ email_send(&hdr,&body,0,0);
1590
+ blob_truncate(&hdr);
1591
+ blob_truncate(&body);
1592
+ }
1593
+ blob_zero(&hdr);
1594
+ blob_zero(&body);
1595
+ db_finalize(&q);
1596
+ email_free_eventlist(pEvents);
1597
+ if( (flags & SENDALERT_PRESERVE)==0 ){
1598
+ if( flags & SENDALERT_DIGEST ){
1599
+ db_multi_exec("UPDATE pending_alert SET sentDigest=true");
1600
+ }else{
1601
+ db_multi_exec("UPDATE pending_alert SET sentSep=true");
1602
+ }
1603
+ db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
1604
+ }
1605
+send_alerts_done:
1606
+ db_end_transaction(0);
1607
+}
5271608
--- src/email.c
+++ src/email.c
@@ -19,10 +19,15 @@
19 */
20 #include "config.h"
21 #include "email.h"
22 #include <assert.h>
23
 
 
 
 
 
24 /*
25 ** SQL code to implement the tables needed by the email notification
26 ** system.
27 */
28 static const char zEmailInit[] =
@@ -42,34 +47,34 @@
42 @ -- we might also add a separate table that allows subscribing to email
43 @ -- notifications for specific branches or tags or tickets.
44 @ --
45 @ CREATE TABLE repository.subscriber(
46 @ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use
47 @ subscriberCode TEXT UNIQUE, -- UUID for subscriber. External use
48 @ sname TEXT, -- Human readable name
49 @ suname TEXT, -- Corresponding USER or NULL
50 @ semail TEXT, -- email address
51 @ sverify BOOLEAN, -- email address verified
52 @ sdonotcall BOOLEAN, -- true for Do Not Call
53 @ sdigest BOOLEAN, -- true for daily digests only
54 @ ssub TEXT, -- baseline subscriptions
55 @ sctime DATE, -- When this entry was created. JulianDay
56 @ smtime DATE, -- Last change. JulianDay
57 @ sipaddr TEXT, -- IP address for last change
58 @ spswdHash TEXT -- SHA3 hash of password
59 @ );
 
 
60 @
61 @ -- Email notifications that need to be sent.
62 @ --
63 @ -- If the eventid key is an integer, then it corresponds to the
64 @ -- EVENT.OBJID table. Other kinds of eventids are reserved for
65 @ -- future expansion.
66 @ --
67 @ CREATE TABLE repository.email_pending(
68 @ eventid ANY PRIMARY KEY, -- Object that changed
69 @ sentSep BOOLEAN DEFAULT false, -- individual emails sent
70 @ sentDigest BOOLEAN DEFAULT false -- digest emails sent
71 @ ) WITHOUT ROWID;
72 @
73 @ -- Record bounced emails. If too many bounces are received within
74 @ -- some defined time range, then cancel the subscription. Older
75 @ -- entries are periodically purged.
@@ -85,10 +90,66 @@
85 ** Make sure the unversioned table exists in the repository.
86 */
87 void email_schema(void){
88 if( !db_table_exists("repository", "subscriber") ){
89 db_multi_exec(zEmailInit/*works-like:""*/);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90 }
91 }
92
93
94 /*
@@ -99,64 +160,96 @@
99 void setup_email(void){
100 static const char *const azSendMethods[] = {
101 "off", "Disabled",
102 "pipe", "Pipe to a command",
103 "db", "Store in a database",
104 "file", "Store in a directory"
105 };
106 login_check_credentials();
107 if( !g.perm.Setup ){
108 login_needed(0);
109 return;
110 }
111 db_begin_transaction();
112
 
113 style_header("Email Notification Setup");
114 @ <form action="%R/setup_email" method="post"><div>
115 @ <input type="submit" name="submit" value="Apply Changes" /><hr>
116 login_insert_csrf_secret();
117 multiple_choice_attribute("Email Send Method","email-send-method",
118 "esm", "off", count(azSendMethods)/2, azSendMethods);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119 @ <p>How to send email. The "Pipe to a command"
120 @ method is the usual choice in production.
121 @ (Property: "email-send-method")</p>
122 @ <hr>
123 entry_attribute("Command To Pipe Email To", 80, "esc",
124 "email-send-command", "sendmail -t", 0);
 
 
125 @ <p>When the send method is "pipe to a command", this is the command
126 @ that is run. Email messages are piped into the standard input of this
127 @ command. The command is expected to extract the sender address,
128 @ recepient addresses, and subject from the header of the piped email
129 @ text. (Property: "email-send-command")</p>
130
131 entry_attribute("Database In Which To Store Email", 60, "esdb",
132 "email-send-db", "", 0);
133 @ <p>When the send method is "store in a databaes", each email message is
134 @ stored in an SQLite database file with the name given here.
135 @ (Property: "email-send-db")</p>
136
137 entry_attribute("Directory In Which To Store Email", 60, "esdir",
138 "email-send-dir", "", 0);
139 @ <p>When the send method is "store in a directory", each email message is
140 @ stored as a separate file in the directory shown here.
141 @ (Property: "email-send-dir")</p>
142 @ <hr>
143
144 entry_attribute("\"From\" email address", 40, "ef",
145 "email-self", "", 0);
146 @ <p>This is the email from which email notifications are sent. The
147 @ system administrator should arrange for emails sent to this address
148 @ to be handed off to the "fossil email incoming" command so that Fossil
149 @ can handle bounces. (Property: "email-self")</p>
150 @ <hr>
151
152 entry_attribute("Administrator email address", 40, "ea",
153 "email-admin", "", 0);
154 @ <p>This is the email for the human administrator for the system.
155 @ Abuse and trouble reports are send here.
156 @ (Property: "email-admin")</p>
157 @ <hr>
 
 
 
 
 
 
 
 
 
158 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
159 @ </div></form>
160 db_end_transaction(0);
161 style_footer();
162 }
@@ -172,10 +265,23 @@
172 k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf);
173 blob_append(pOut, zBuf, k);
174 blob_append(pOut, "\r\n", 2);
175 }
176 }
 
 
 
 
 
 
 
 
 
 
 
 
 
177
178 #if defined(_WIN32) || defined(WIN32)
179 # undef popen
180 # define popen _popen
181 # undef pclose
@@ -231,18 +337,20 @@
231 zBoundary = db_text(0, "SELECT hex(randomblob(20))");
232 blob_appendf(&all, "Content-Type: multipart/alternative;"
233 " boundary=\"%s\"\r\n", zBoundary);
234 }
235 if( pPlain ){
 
236 if( zBoundary ){
237 blob_appendf(&all, "\r\n--%s\r\n", zBoundary);
238 }
239 blob_appendf(&all,"Content-Type: text/plain\r\n");
240 blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
241 append_base64(&all, pPlain);
242 }
243 if( pHtml ){
 
244 if( zBoundary ){
245 blob_appendf(&all, "--%s\r\n", zBoundary);
246 }
247 blob_appendf(&all,"Content-Type: text/html\r\n");
248 blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
@@ -281,20 +389,32 @@
281 fclose(out);
282 }
283 }
284 }else if( strcmp(zDest, "dir")==0 ){
285 const char *zDir = db_get("email-send-dir","./");
286 char *zFile = db_text(0,
287 "SELECT %Q||strftime('/%%Y%%m%%d%%H%%M%%S','now')||hex(randomblob(8))",
288 zDir);
289 blob_write_to_file(&all, zFile);
290 fossil_free(zFile);
291 }else if( strcmp(zDest, "stdout")==0 ){
292 fossil_print("%s\n", blob_str(&all));
293 }
294 blob_zero(&all);
295 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
297 /*
298 ** SETTING: email-send-method width=5 default=off
299 ** Determine the method used to send email. Allowed values are
300 ** "off", "pipe", "dir", "db", and "stdout". The "off" value means
@@ -326,19 +446,37 @@
326 /*
327 ** SETTING: email-self width=40
328 ** This is the email address for the repository. Outbound emails add
329 ** this email address as the "From:" field.
330 */
 
 
 
 
 
 
331
332
333 /*
334 ** COMMAND: email
335 **
336 ** Usage: %fossil email SUBCOMMAND ARGS...
337 **
338 ** Subcommands:
339 **
 
 
 
 
 
 
 
 
 
 
 
 
340 ** reset Hard reset of all email notification tables
341 ** in the repository. This erases all subscription
342 ** information. Use with extreme care.
343 **
344 ** send TO [OPTIONS] Send a single email message using whatever
@@ -351,37 +489,71 @@
351 ** --stdout
352 ** --subject|-S SUBJECT
353 **
354 ** settings [NAME VALUE] With no arguments, list all email settings.
355 ** Or change the value of a single email setting.
 
 
 
 
356 */
357 void email_cmd(void){
358 const char *zCmd;
359 int nCmd;
360 db_find_and_open_repository(0, 0);
361 email_schema();
362 zCmd = g.argc>=3 ? g.argv[2] : "x";
363 nCmd = (int)strlen(zCmd);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364 if( strncmp(zCmd, "reset", nCmd)==0 ){
365 Blob yn;
366 int c;
367 fossil_print(
368 "This will erase all content in the repository tables, thus\n"
369 "deleting all subscriber information. The information will be\n"
370 "unrecoverable.\n");
371 prompt_user("Continue? (y/N) ", &yn);
372 c = blob_str(&yn)[0];
 
 
 
 
 
 
 
 
373 if( c=='y' ){
 
374 db_multi_exec(
375 "DROP TABLE IF EXISTS subscriber;\n"
376 "DROP TABLE IF EXISTS subscription;\n"
 
 
377 "DROP TABLE IF EXISTS email_pending;\n"
378 "DROP TABLE IF EXISTS email_bounce;\n"
379 );
380 email_schema();
381 }
382 blob_zero(&yn);
383 }else
384 if( strncmp(zCmd, "send", nCmd)==0 ){
385 Blob prompt, body, hdr;
386 int sendAsBoth = find_option("both",0,0)!=0;
387 int sendAsHtml = find_option("html",0,0)!=0;
@@ -402,10 +574,11 @@
402 if( zSource ){
403 blob_read_from_file(&body, zSource, ExtFILE);
404 }else{
405 prompt_for_user_comment(&body, &prompt);
406 }
 
407 if( sendAsHtml ){
408 email_send(&hdr, 0, &body, zDest);
409 }else if( sendAsBoth ){
410 Blob html;
411 blob_init(&html, 0, 0);
@@ -416,14 +589,13 @@
416 email_send(&hdr, &body, 0, zDest);
417 }
418 blob_zero(&hdr);
419 blob_zero(&body);
420 blob_zero(&prompt);
421 }
422 else if( strncmp(zCmd, "settings", nCmd)==0 ){
423 int isGlobal = find_option("global",0,0)!=0;
424 int i;
425 int nSetting;
426 const Setting *pSetting = setting_info(&nSetting);
427 db_open_config(1, 0);
428 verify_all_options();
429 if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]");
@@ -439,15 +611,133 @@
439 pSetting = setting_info(&nSetting);
440 for(; nSetting>0; nSetting--, pSetting++ ){
441 if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
442 print_setting(pSetting);
443 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444 }
445 else{
446 usage("reset|send|setting");
 
 
 
 
447 }
 
 
 
448 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
450 /*
451 ** WEBPAGE: subscribe
452 **
453 ** Allow users to subscribe to email notifications, or to change or
@@ -455,65 +745,162 @@
455 */
456 void subscribe_page(void){
457 int needCaptcha;
458 unsigned int uSeed;
459 const char *zDecoded;
460 char *zCaptcha;
 
 
461
462 login_check_credentials();
463 if( !g.perm.EmailAlert ){
464 login_needed(g.anon.EmailAlert);
465 return;
466 }
467 style_header("Email Subscription");
468 needCaptcha = P("usecaptcha")!=0 || login_is_nobody()
469 || login_is_special(g.zLogin);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470 form_begin(0, "%R/subscribe");
471 @ <table class="subscribe">
472 @ <tr>
473 @ <td class="form_label">Email&nbsp;Address:</td>
474 @ <td><input type="text" name="e" value="" size="30"></td>
475 @ <td></td>
 
 
476 @ </tr>
477 if( needCaptcha ){
478 uSeed = captcha_seed();
479 zDecoded = captcha_decode(uSeed);
480 zCaptcha = captcha_render(zDecoded);
481 @ <tr>
482 @ <td class="form_label">Security Code:</td>
483 @ <td><input type="text" name="captcha" value="" size="30">
484 @ <input type="hidden" name="usecaptcha" value="1"></td>
485 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
486 @ <td><span class="optionalTag">(copy from below)</span></td>
487 @ </tr>
488 }
489 @ <tr>
490 @ <td class="form_label">Nickname:</td>
491 @ <td><input type="text" name="nn" value="" size="30"></td>
492 @ <td><span class="optionalTag">(optional)</span></td>
493 @ </tr>
494 @ <tr>
495 @ <td class="form_label">Password:</td>
496 @ <td><input type="password" name="pw" value="" size="30"></td>
497 @ <td><span class="optionalTag">(optional)</span></td>
498 @ </tr>
499 @ <tr>
500 @ <td class="form_label">Options:</td>
501 @ <td><label><input type="checkbox" name="sa" value="0">\
502 @ Announcements</label><br>
503 @ <label><input type="checkbox" name="sc" value="0">\
504 @ Check-ins</label><br>
505 @ <label><input type="checkbox" name="st" value="0">\
506 @ Ticket changes</label><br>
507 @ <label><input type="checkbox" name="sw" value="0">\
508 @ Wiki</label><br>
509 @ <label><input type="checkbox" name="di" value="0">\
510 @ Daily digest only</label><br></td>
 
 
 
 
 
 
 
 
511 @ </tr>
512 @ <tr>
513 @ <td></td>
514 @ <td><input type="submit" value="Submit"></td>
 
 
 
 
 
515 @ </tr>
516 @ </table>
517 if( needCaptcha ){
518 @ <div class="captcha"><table class="captcha"><tr><td><pre>
519 @ %h(zCaptcha)
@@ -520,7 +907,701 @@
520 @ </pre>
521 @ Enter the 8 characters above in the "Security Code" box
522 @ </td></tr></table></div>
523 }
524 @ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525 style_footer();
526 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
--- src/email.c
+++ src/email.c
@@ -19,10 +19,15 @@
19 */
20 #include "config.h"
21 #include "email.h"
22 #include <assert.h>
23
24 /*
25 ** Maximum size of the subscriberCode blob, in bytes
26 */
27 #define SUBSCRIBER_CODE_SZ 32
28
29 /*
30 ** SQL code to implement the tables needed by the email notification
31 ** system.
32 */
33 static const char zEmailInit[] =
@@ -42,34 +47,34 @@
47 @ -- we might also add a separate table that allows subscribing to email
48 @ -- notifications for specific branches or tags or tickets.
49 @ --
50 @ CREATE TABLE repository.subscriber(
51 @ subscriberId INTEGER PRIMARY KEY, -- numeric subscriber ID. Internal use
52 @ subscriberCode BLOB UNIQUE, -- UUID for subscriber. External use
53 @ semail TEXT UNIQUE COLLATE nocase,-- email address
54 @ suname TEXT, -- corresponding USER entry
55 @ sverified BOOLEAN, -- email address verified
 
56 @ sdonotcall BOOLEAN, -- true for Do Not Call
57 @ sdigest BOOLEAN, -- true for daily digests only
58 @ ssub TEXT, -- baseline subscriptions
59 @ sctime DATE, -- When this entry was created. JulianDay
60 @ smtime DATE, -- Last change. JulianDay
61 @ smip TEXT -- IP address of last change
 
62 @ );
63 @ CREATE INDEX repository.subscriberUname
64 @ ON subscriber(suname) WHERE suname IS NOT NULL;
65 @
66 @ -- Email notifications that need to be sent.
67 @ --
68 @ -- The first character of the eventid determines the event type.
69 @ -- Remaining characters determine the specific event. For example,
70 @ -- 'c4413' means check-in with rid=4413.
71 @ --
72 @ CREATE TABLE repository.pending_alert(
73 @ eventid TEXT PRIMARY KEY, -- Object that changed
74 @ sentSep BOOLEAN DEFAULT false, -- individual emails sent
75 @ sendDigest BOOLEAN DEFAULT false -- digest emails sent
76 @ ) WITHOUT ROWID;
77 @
78 @ -- Record bounced emails. If too many bounces are received within
79 @ -- some defined time range, then cancel the subscription. Older
80 @ -- entries are periodically purged.
@@ -85,10 +90,66 @@
90 ** Make sure the unversioned table exists in the repository.
91 */
92 void email_schema(void){
93 if( !db_table_exists("repository", "subscriber") ){
94 db_multi_exec(zEmailInit/*works-like:""*/);
95 email_triggers_enable();
96 }
97 }
98
99 /*
100 ** Enable triggers that automatically populate the event_pending
101 ** table.
102 */
103 void email_triggers_enable(void){
104 if( !db_table_exists("repository","pending_alert") ) return;
105 db_multi_exec(
106 "CREATE TRIGGER IF NOT EXISTS repository.email_trigger1\n"
107 "AFTER INSERT ON event BEGIN\n"
108 " INSERT INTO pending_alert(eventid,mtime)\n"
109 " SELECT printf('%%.1c%%d',new.type,new.objid),"
110 " julianday('now') WHERE true\n"
111 " ON CONFLICT(eventId) DO NOTHING;\n"
112 "END;"
113 );
114 }
115
116 /*
117 ** Disable triggers the event_pending triggers.
118 **
119 ** This must be called before rebuilding the EVENT table, for example
120 ** via the "fossil rebuild" command.
121 */
122 void email_triggers_disable(void){
123 db_multi_exec(
124 "DROP TRIGGER IF EXISTS repository.email_trigger1;\n"
125 );
126 }
127
128 /*
129 ** Return true if email alerts are active.
130 */
131 int email_enabled(void){
132 if( !db_table_exists("repository", "subscriber") ) return 0;
133 if( fossil_strcmp(db_get("email-send-method","off"),"off")==0 ) return 0;
134 return 1;
135 }
136
137
138
139 /*
140 ** Insert a "Subscriber List" submenu link if the current user
141 ** is an administrator.
142 */
143 void email_submenu_common(void){
144 if( g.perm.Admin ){
145 if( fossil_strcmp(g.zPath,"subscribers") ){
146 style_submenu_element("List Subscribers","%R/subscribers");
147 }
148 if( fossil_strcmp(g.zPath,"subscribe") ){
149 style_submenu_element("Add New Subscriber","%R/subscribe");
150 }
151 }
152 }
153
154
155 /*
@@ -99,64 +160,96 @@
160 void setup_email(void){
161 static const char *const azSendMethods[] = {
162 "off", "Disabled",
163 "pipe", "Pipe to a command",
164 "db", "Store in a database",
165 "dir", "Store in a directory"
166 };
167 login_check_credentials();
168 if( !g.perm.Setup ){
169 login_needed(0);
170 return;
171 }
172 db_begin_transaction();
173
174 email_submenu_common();
175 style_header("Email Notification Setup");
176 @ <form action="%R/setup_email" method="post"><div>
177 @ <input type="submit" name="submit" value="Apply Changes" /><hr>
178 login_insert_csrf_secret();
179
180 entry_attribute("Canonical Server URL", 40, "email-url",
181 "eurl", "", 0);
182 @ <p><b>Required.</b>
183 @ This is URL used as the basename for hyperlinks included in
184 @ email alert text. Omit the trailing "/".
185 @ Suggested value: "%h(g.zBaseURL)"
186 @ (Property: "email-url")</p>
187 @ <hr>
188
189 entry_attribute("\"From\" email address", 20, "email-self",
190 "eself", "", 0);
191 @ <p><b>Required.</b>
192 @ This is the email from which email notifications are sent. The
193 @ system administrator should arrange for emails sent to this address
194 @ to be handed off to the "fossil email incoming" command so that Fossil
195 @ can handle bounces. (Property: "email-self")</p>
196 @ <hr>
197
198 entry_attribute("Repository Nickname", 16, "email-subname",
199 "enn", "", 0);
200 @ <p><b>Required.</b>
201 @ This is short name used to identifies the repository in the
202 @ Subject: line of email alerts. Traditionally this name is
203 @ included in square brackets. Examples: "[fossil-src]", "[sqlite-src]".
204 @ (Property: "email-subname")</p>
205 @ <hr>
206
207 multiple_choice_attribute("Email Send Method", "email-send-method", "esm",
208 "off", count(azSendMethods)/2, azSendMethods);
209 @ <p>How to send email. The "Pipe to a command"
210 @ method is the usual choice in production.
211 @ (Property: "email-send-method")</p>
212 @ <hr>
213
214
215 entry_attribute("Command To Pipe Email To", 80, "email-send-command",
216 "ecmd", "sendmail -t", 0);
217 @ <p>When the send method is "pipe to a command", this is the command
218 @ that is run. Email messages are piped into the standard input of this
219 @ command. The command is expected to extract the sender address,
220 @ recepient addresses, and subject from the header of the piped email
221 @ text. (Property: "email-send-command")</p>
222
223 entry_attribute("Database In Which To Store Email", 60, "email-send-db",
224 "esdb", "", 0);
225 @ <p>When the send method is "store in a databaes", each email message is
226 @ stored in an SQLite database file with the name given here.
227 @ (Property: "email-send-db")</p>
228
229 entry_attribute("Directory In Which To Store Email", 60, "email-send-dir",
230 "esdir", "", 0);
231 @ <p>When the send method is "store in a directory", each email message is
232 @ stored as a separate file in the directory shown here.
233 @ (Property: "email-send-dir")</p>
234 @ <hr>
235
236 entry_attribute("Administrator email address", 40, "email-admin",
237 "eadmin", "", 0);
 
 
 
 
 
 
 
 
238 @ <p>This is the email for the human administrator for the system.
239 @ Abuse and trouble reports are send here.
240 @ (Property: "email-admin")</p>
241 @ <hr>
242
243 entry_attribute("Inbound email directory", 40, "email-receive-dir",
244 "erdir", "", 0);
245 @ <p>Inbound emails can be stored in a directory for analysis as
246 @ a debugging aid. Put the name of that directory in this entry box.
247 @ Disable saving of inbound email by making this an empty string.
248 @ Abuse and trouble reports are send here.
249 @ (Property: "email-receive-dir")</p>
250 @ <hr>
251 @ <p><input type="submit" name="submit" value="Apply Changes" /></p>
252 @ </div></form>
253 db_end_transaction(0);
254 style_footer();
255 }
@@ -172,10 +265,23 @@
265 k = translateBase64(blob_buffer(pMsg)+i, i+54<n ? 54 : n-i, zBuf);
266 blob_append(pOut, zBuf, k);
267 blob_append(pOut, "\r\n", 2);
268 }
269 }
270
271 /*
272 ** Come up with a unique filename in the zDir directory.
273 **
274 ** Space to hold the filename is obtained from mprintf() and must
275 ** be freed using fossil_free() by the caller.
276 */
277 static char *emailTempFilename(const char *zDir){
278 char *zFile = db_text(0,
279 "SELECT %Q||strftime('/%%Y%%m%%d%%H%%M%%S-','now')||hex(randomblob(8))",
280 zDir);
281 return zFile;
282 }
283
284 #if defined(_WIN32) || defined(WIN32)
285 # undef popen
286 # define popen _popen
287 # undef pclose
@@ -231,18 +337,20 @@
337 zBoundary = db_text(0, "SELECT hex(randomblob(20))");
338 blob_appendf(&all, "Content-Type: multipart/alternative;"
339 " boundary=\"%s\"\r\n", zBoundary);
340 }
341 if( pPlain ){
342 blob_add_final_newline(pPlain);
343 if( zBoundary ){
344 blob_appendf(&all, "\r\n--%s\r\n", zBoundary);
345 }
346 blob_appendf(&all,"Content-Type: text/plain\r\n");
347 blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
348 append_base64(&all, pPlain);
349 }
350 if( pHtml ){
351 blob_add_final_newline(pHtml);
352 if( zBoundary ){
353 blob_appendf(&all, "--%s\r\n", zBoundary);
354 }
355 blob_appendf(&all,"Content-Type: text/html\r\n");
356 blob_appendf(&all, "Content-Transfer-Encoding: base64\r\n\r\n");
@@ -281,20 +389,32 @@
389 fclose(out);
390 }
391 }
392 }else if( strcmp(zDest, "dir")==0 ){
393 const char *zDir = db_get("email-send-dir","./");
394 char *zFile = emailTempFilename(zDir);
 
 
395 blob_write_to_file(&all, zFile);
396 fossil_free(zFile);
397 }else if( strcmp(zDest, "stdout")==0 ){
398 fossil_print("%s\n", blob_str(&all));
399 }
400 blob_zero(&all);
401 }
402
403 /*
404 ** Analyze and act on a received email.
405 **
406 ** This routine takes ownership of the Blob parameter and is responsible
407 ** for freeing that blob when it is done with it.
408 **
409 ** This routine acts on all email messages received from the
410 ** "fossil email inbound" command.
411 */
412 void email_receive(Blob *pMsg){
413 /* To Do: Look for bounce messages and possibly disable subscriptions */
414 blob_zero(pMsg);
415 }
416
417 /*
418 ** SETTING: email-send-method width=5 default=off
419 ** Determine the method used to send email. Allowed values are
420 ** "off", "pipe", "dir", "db", and "stdout". The "off" value means
@@ -326,19 +446,37 @@
446 /*
447 ** SETTING: email-self width=40
448 ** This is the email address for the repository. Outbound emails add
449 ** this email address as the "From:" field.
450 */
451 /*
452 ** SETTING: email-receive-dir width=40
453 ** Inbound email messages are saved as separate files in this directory,
454 ** for debugging analysis. Disable saving of inbound emails omitting
455 ** this setting, or making it an empty string.
456 */
457
458
459 /*
460 ** COMMAND: email
461 **
462 ** Usage: %fossil email SUBCOMMAND ARGS...
463 **
464 ** Subcommands:
465 **
466 ** exec Compose and send pending email alerts.
467 ** Some installations may want to do this via
468 ** a cron-job to make sure alerts are sent
469 ** in a timely manner.
470 ** Options:
471 **
472 ** --digest Send digests
473 **
474 ** inbound [FILE] Receive an inbound email message. This message
475 ** is analyzed to see if it is a bounce, and if
476 ** necessary, subscribers may be disabled.
477 **
478 ** reset Hard reset of all email notification tables
479 ** in the repository. This erases all subscription
480 ** information. Use with extreme care.
481 **
482 ** send TO [OPTIONS] Send a single email message using whatever
@@ -351,37 +489,71 @@
489 ** --stdout
490 ** --subject|-S SUBJECT
491 **
492 ** settings [NAME VALUE] With no arguments, list all email settings.
493 ** Or change the value of a single email setting.
494 **
495 ** subscribers [PATTERN] List all subscribers matching PATTERN.
496 **
497 ** unsubscribe EMAIL Remove a single subscriber with the given EMAIL.
498 */
499 void email_cmd(void){
500 const char *zCmd;
501 int nCmd;
502 db_find_and_open_repository(0, 0);
503 email_schema();
504 zCmd = g.argc>=3 ? g.argv[2] : "x";
505 nCmd = (int)strlen(zCmd);
506 if( strncmp(zCmd, "exec", nCmd)==0 ){
507 u32 eFlags = 0;
508 if( find_option("digest",0,0)!=0 ) eFlags |= SENDALERT_DIGEST;
509 verify_all_options();
510 email_send_alerts(eFlags);
511 }else
512 if( strncmp(zCmd, "inbound", nCmd)==0 ){
513 Blob email;
514 const char *zInboundDir = db_get("email-receive-dir","");
515 verify_all_options();
516 if( g.argc!=3 && g.argc!=4 ){
517 usage("inbound [FILE]");
518 }
519 blob_read_from_file(&email, g.argc==3 ? "-" : g.argv[3], ExtFILE);
520 if( zInboundDir[0] ){
521 char *zFN = emailTempFilename(zInboundDir);
522 blob_write_to_file(&email, zFN);
523 fossil_free(zFN);
524 }
525 email_receive(&email);
526 }else
527 if( strncmp(zCmd, "reset", nCmd)==0 ){
 
528 int c;
529 int bForce = find_option("force","f",0)!=0;
530 verify_all_options();
531 if( bForce ){
532 c = 'y';
533 }else{
534 Blob yn;
535 fossil_print(
536 "This will erase all content in the repository tables, thus\n"
537 "deleting all subscriber information. The information will be\n"
538 "unrecoverable.\n");
539 prompt_user("Continue? (y/N) ", &yn);
540 c = blob_str(&yn)[0];
541 blob_zero(&yn);
542 }
543 if( c=='y' ){
544 email_triggers_disable();
545 db_multi_exec(
546 "DROP TABLE IF EXISTS subscriber;\n"
547 "DROP TABLE IF EXISTS pending_alert;\n"
548 "DROP TABLE IF EXISTS email_bounce;\n"
549 /* Legacy */
550 "DROP TABLE IF EXISTS email_pending;\n"
551 "DROP TABLE IF EXISTS subscription;\n"
552 );
553 email_schema();
554 }
 
555 }else
556 if( strncmp(zCmd, "send", nCmd)==0 ){
557 Blob prompt, body, hdr;
558 int sendAsBoth = find_option("both",0,0)!=0;
559 int sendAsHtml = find_option("html",0,0)!=0;
@@ -402,10 +574,11 @@
574 if( zSource ){
575 blob_read_from_file(&body, zSource, ExtFILE);
576 }else{
577 prompt_for_user_comment(&body, &prompt);
578 }
579 blob_add_final_newline(&body);
580 if( sendAsHtml ){
581 email_send(&hdr, 0, &body, zDest);
582 }else if( sendAsBoth ){
583 Blob html;
584 blob_init(&html, 0, 0);
@@ -416,14 +589,13 @@
589 email_send(&hdr, &body, 0, zDest);
590 }
591 blob_zero(&hdr);
592 blob_zero(&body);
593 blob_zero(&prompt);
594 }else
595 if( strncmp(zCmd, "settings", nCmd)==0 ){
596 int isGlobal = find_option("global",0,0)!=0;
 
597 int nSetting;
598 const Setting *pSetting = setting_info(&nSetting);
599 db_open_config(1, 0);
600 verify_all_options();
601 if( g.argc!=3 && g.argc!=5 ) usage("setting [NAME VALUE]");
@@ -439,15 +611,133 @@
611 pSetting = setting_info(&nSetting);
612 for(; nSetting>0; nSetting--, pSetting++ ){
613 if( strncmp(pSetting->name,"email-",6)!=0 ) continue;
614 print_setting(pSetting);
615 }
616 }else
617 if( strncmp(zCmd, "subscribers", nCmd)==0 ){
618 Stmt q;
619 verify_all_options();
620 if( g.argc!=3 && g.argc!=4 ) usage("subscribers [PATTERN]");
621 if( g.argc==4 ){
622 char *zPattern = g.argv[3];
623 db_prepare(&q,
624 "SELECT semail FROM subscriber"
625 " WHERE semail LIKE '%%%q%%' OR suname LIKE '%%%q%%'"
626 " OR semail GLOB '*%q*' or suname GLOB '*%q*'"
627 " ORDER BY semail",
628 zPattern, zPattern, zPattern, zPattern);
629 }else{
630 db_prepare(&q,
631 "SELECT semail FROM subscriber"
632 " ORDER BY semail");
633 }
634 while( db_step(&q)==SQLITE_ROW ){
635 fossil_print("%s\n", db_column_text(&q, 0));
636 }
637 db_finalize(&q);
638 }else
639 if( strncmp(zCmd, "unsubscribe", nCmd)==0 ){
640 verify_all_options();
641 if( g.argc!=4 ) usage("unsubscribe EMAIL");
642 db_multi_exec(
643 "DELETE FROM subscriber WHERE semail=%Q", g.argv[3]);
644 }else
645 {
646 usage("exec|inbound|reset|send|setting|subscribers|unsubscribe");
647 }
648 }
649
650 /*
651 ** Do error checking on a submitted subscription form. Return TRUE
652 ** if the submission is valid. Return false if any problems are seen.
653 */
654 static int subscribe_error_check(
655 int *peErr, /* Type of error */
656 char **pzErr, /* Error message text */
657 int needCaptcha /* True if captcha check needed */
658 ){
659 const char *zEAddr;
660 int i, j, n;
661 char c;
662
663 *peErr = 0;
664 *pzErr = 0;
665
666 /* Check the validity of the email address.
667 **
668 ** (1) Exactly one '@' character.
669 ** (2) No other characters besides [a-zA-Z0-9._-]
670 */
671 zEAddr = P("e");
672 if( zEAddr==0 ) return 0;
673 for(i=j=0; (c = zEAddr[i])!=0; i++){
674 if( c=='@' ){
675 n = i;
676 j++;
677 continue;
678 }
679 if( !fossil_isalnum(c) && c!='.' && c!='_' && c!='-' ){
680 *peErr = 1;
681 *pzErr = mprintf("illegal character in email address: 0x%x '%c'",
682 c, c);
683 return 0;
684 }
685 }
686 if( j!=1 ){
687 *peErr = 1;
688 *pzErr = mprintf("email address should contain exactly one '@'");
689 return 0;
690 }
691 if( n<1 ){
692 *peErr = 1;
693 *pzErr = mprintf("name missing before '@' in email address");
694 return 0;
695 }
696 if( n>i-5 ){
697 *peErr = 1;
698 *pzErr = mprintf("email domain too short");
699 return 0;
700 }
701
702 /* Verify the captcha */
703 if( needCaptcha && !captcha_is_correct(1) ){
704 *peErr = 2;
705 *pzErr = mprintf("incorrect security code");
706 return 0;
707 }
708
709 /* Check to make sure the email address is available for reuse */
710 if( db_exists("SELECT 1 FROM subscriber WHERE semail=%Q", zEAddr) ){
711 *peErr = 1;
712 *pzErr = mprintf("this email address is used by someone else");
713 return 0;
714 }
715
716 /* If we reach this point, all is well */
717 return 1;
718 }
719
720 /*
721 ** Text of email message sent in order to confirm a subscription.
722 */
723 static const char zConfirmMsg[] =
724 @ Someone has signed you up for email alerts on the Fossil repository
725 @ at %s.
726 @
727 @ To confirm your subscription and begin receiving alerts, click on
728 @ the following hyperlink:
729 @
730 @ %s/alerts/%s
731 @
732 @ Save the hyperlink above! You can reuse this same hyperlink to
733 @ unsubscribe or to change the kinds of alerts you receive.
734 @
735 @ If you do not want to subscribe, you can simply ignore this message.
736 @ You will not be contacted again.
737 @
738 ;
739
740 /*
741 ** WEBPAGE: subscribe
742 **
743 ** Allow users to subscribe to email notifications, or to change or
@@ -455,65 +745,162 @@
745 */
746 void subscribe_page(void){
747 int needCaptcha;
748 unsigned int uSeed;
749 const char *zDecoded;
750 char *zCaptcha = 0;
751 char *zErr = 0;
752 int eErr = 0;
753
754 login_check_credentials();
755 if( !g.perm.EmailAlert ){
756 login_needed(g.anon.EmailAlert);
757 return;
758 }
759 if( login_is_individual()
760 && db_exists("SELECT 1 FROM subscriber WHERE suname=%Q",g.zLogin)
761 ){
762 /* This person is already signed up for email alerts. Jump
763 ** to the screen that lets them edit their alert preferences.
764 ** Except, administrators can create subscriptions for others so
765 ** do not jump for them.
766 */
767 if( g.perm.Admin ){
768 /* Admins get a link to admin their own account, but they
769 ** stay on this page so that they can create subscriptions
770 ** for other people. */
771 style_submenu_element("My Subscription","%R/alerts");
772 }else{
773 /* Everybody else jumps to the page to administer their own
774 ** account only. */
775 cgi_redirectf("%R/alerts");
776 return;
777 }
778 }
779 email_submenu_common();
780 needCaptcha = !login_is_individual();
781 if( P("submit")
782 && cgi_csrf_safe(1)
783 && subscribe_error_check(&eErr,&zErr,needCaptcha)
784 ){
785 /* A validated request for a new subscription has been received. */
786 char ssub[20];
787 const char *zEAddr = P("e");
788 sqlite3_int64 id; /* New subscriber Id */
789 const char *zCode; /* New subscriber code (in hex) */
790 int nsub = 0;
791 const char *suname = PT("suname");
792 if( suname==0 && needCaptcha==0 && !g.perm.Admin ) suname = g.zLogin;
793 if( suname && suname[0]==0 ) suname = 0;
794 if( PB("sa") ) ssub[nsub++] = 'a';
795 if( PB("sc") ) ssub[nsub++] = 'c';
796 if( PB("st") ) ssub[nsub++] = 't';
797 if( PB("sw") ) ssub[nsub++] = 'w';
798 ssub[nsub] = 0;
799 db_multi_exec(
800 "INSERT INTO subscriber(subscriberCode,semail,suname,"
801 " sverified,sdonotcall,sdigest,ssub,sctime,smtime,smip)"
802 "VALUES(randomblob(32),%Q,%Q,%d,0,%d,%Q,"
803 " julianday('now'),julianday('now'),%Q)",
804 /* semail */ zEAddr,
805 /* suname */ suname,
806 /* sverified */ needCaptcha==0,
807 /* sdigest */ PB("di"),
808 /* ssub */ ssub,
809 /* smip */ g.zIpAddr
810 );
811 id = db_last_insert_rowid();
812 zCode = db_text(0,
813 "SELECT hex(subscriberCode) FROM subscriber WHERE subscriberId=%lld",
814 id);
815 if( !needCaptcha ){
816 /* The new subscription has been added on behalf of a logged-in user.
817 ** No verification is required. Jump immediately to /alerts page.
818 */
819 cgi_redirectf("%R/alerts/%s", zCode);
820 return;
821 }else{
822 /* We need to send a verification email */
823 Blob hdr, body;
824 blob_init(&hdr,0,0);
825 blob_init(&body,0,0);
826 blob_appendf(&hdr, "To: %s\n", zEAddr);
827 blob_appendf(&hdr, "Subject: Subscription verification\n");
828 blob_appendf(&body, zConfirmMsg/*works-like:"%s%s%s"*/,
829 g.zBaseURL, g.zBaseURL, zCode);
830 email_send(&hdr, &body, 0, 0);
831 style_header("Email Alert Verification");
832 @ <p>An email has been sent to "%h(zEAddr)". That email contains a
833 @ hyperlink that you must click on in order to activate your
834 @ subscription.</p>
835 style_footer();
836 }
837 return;
838 }
839 style_header("Signup For Email Alerts");
840 @ <p>To receive email notifications for changes to this
841 @ repository, fill out the form below and press "Submit" button.</p>
842 form_begin(0, "%R/subscribe");
843 @ <table class="subscribe">
844 @ <tr>
845 @ <td class="form_label">Email&nbsp;Address:</td>
846 @ <td><input type="text" name="e" value="%h(PD("e",""))" size="30"></td>
847 if( eErr==1 ){
848 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
849 }
850 @ </tr>
851 if( needCaptcha ){
852 uSeed = captcha_seed();
853 zDecoded = captcha_decode(uSeed);
854 zCaptcha = captcha_render(zDecoded);
855 @ <tr>
856 @ <td class="form_label">Security Code:</td>
857 @ <td><input type="text" name="captcha" value="" size="30">
858 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
859 if( eErr==2 ){
860 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
861 }
862 @ </tr>
863 }
864 if( g.perm.Admin ){
865 @ <tr>
866 @ <td class="form_label">User:</td>
867 @ <td><input type="text" name="suname" value="%h(PD("suname",g.zLogin))" \
868 @ size="30"></td>
869 if( eErr==3 ){
870 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
871 }
872 @ </tr>
873 }
874 @ <tr>
875 @ <td class="form_label">Options:</td>
876 @ <td><label><input type="checkbox" name="sa" %s(PCK("sa"))> \
877 @ Announcements</label><br>
878 @ <label><input type="checkbox" name="sc" %s(PCK("sc"))> \
879 @ Check-ins</label><br>
880 @ <label><input type="checkbox" name="st" %s(PCK("st"))> \
881 @ Ticket changes</label><br>
882 @ <label><input type="checkbox" name="sw" %s(PCK("sw"))> \
883 @ Wiki</label><br>
884 @ <label><input type="checkbox" name="di" %s(PCK("di"))> \
885 @ Daily digest only</label><br>
886 if( g.perm.Admin ){
887 @ <label><input type="checkbox" name="vi" %s(PCK("vi"))> \
888 @ Verified</label><br>
889 @ <label><input type="checkbox" name="dnc" %s(PCK("dnc"))> \
890 @ Do not call</label><br>
891 }
892 @ </td>
893 @ </tr>
894 @ <tr>
895 @ <td></td>
896 if( needCaptcha && !email_enabled() ){
897 @ <td><input type="submit" name="submit" value="Submit" disabled>
898 @ (Email current disabled)</td>
899 }else{
900 @ <td><input type="submit" name="submit" value="Submit"></td>
901 }
902 @ </tr>
903 @ </table>
904 if( needCaptcha ){
905 @ <div class="captcha"><table class="captcha"><tr><td><pre>
906 @ %h(zCaptcha)
@@ -520,7 +907,701 @@
907 @ </pre>
908 @ Enter the 8 characters above in the "Security Code" box
909 @ </td></tr></table></div>
910 }
911 @ </form>
912 fossil_free(zErr);
913 style_footer();
914 }
915
916 /*
917 ** Either shutdown or completely delete a subscription entry given
918 ** by the hex value zName. Then paint a webpage that explains that
919 ** the entry has been removed.
920 */
921 static void email_unsubscribe(const char *zName){
922 char *zEmail;
923 zEmail = db_text(0, "SELECT semail FROM subscriber"
924 " WHERE subscriberCode=hextoblob(%Q)", zName);
925 if( zEmail==0 ){
926 style_header("Unsubscribe Fail");
927 @ <p>Unable to locate a subscriber with the requested key</p>
928 }else{
929 db_multi_exec(
930 "DELETE FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
931 zName
932 );
933 style_header("Unsubscribed");
934 @ <p>The "%h(zEmail)" email address has been delisted.
935 @ All traces of that email address have been removed</p>
936 }
937 style_footer();
938 return;
939 }
940
941 /*
942 ** WEBPAGE: alerts
943 **
944 ** Edit email alert and notification settings.
945 **
946 ** The subscriber entry is identified in either of two ways:
947 **
948 ** (1) The name= query parameter contains the subscriberCode.
949 **
950 ** (2) The user is logged into an account other than "nobody" or
951 ** "anonymous". In that case the notification settings
952 ** associated with that account can be edited without needing
953 ** to know the subscriber code.
954 */
955 void alerts_page(void){
956 const char *zName = P("name");
957 Stmt q;
958 int sa, sc, st, sw;
959 int sdigest, sdonotcall, sverified;
960 const char *ssub;
961 const char *semail;
962 const char *smip;
963 const char *suname;
964 int eErr = 0;
965 char *zErr = 0;
966
967 login_check_credentials();
968 if( !g.perm.EmailAlert ){
969 cgi_redirect("subscribe");
970 return;
971 }
972 if( zName==0 && login_is_individual() ){
973 zName = db_text(0, "SELECT hex(subscriberCode) FROM subscriber"
974 " WHERE suname=%Q", g.zLogin);
975 }
976 if( zName==0 || !validate16(zName, -1) ){
977 cgi_redirect("subscribe");
978 return;
979 }
980 email_submenu_common();
981 if( P("submit")!=0 && cgi_csrf_safe(1) ){
982 int sdonotcall = PB("sdonotcall");
983 int sdigest = PB("sdigest");
984 char ssub[10];
985 int nsub = 0;
986 if( PB("sa") ) ssub[nsub++] = 'a';
987 if( PB("sc") ) ssub[nsub++] = 'c';
988 if( PB("st") ) ssub[nsub++] = 't';
989 if( PB("sw") ) ssub[nsub++] = 'w';
990 ssub[nsub] = 0;
991 if( g.perm.Admin ){
992 const char *suname = PT("suname");
993 if( suname && suname[0]==0 ) suname = 0;
994 int sverified = PB("sverified");
995 db_multi_exec(
996 "UPDATE subscriber SET"
997 " sdonotcall=%d,"
998 " sdigest=%d,"
999 " ssub=%Q,"
1000 " smtime=julianday('now'),"
1001 " smip=%Q,"
1002 " suname=%Q,"
1003 " sverified=%d"
1004 " WHERE subscriberCode=hextoblob(%Q)",
1005 sdonotcall,
1006 sdigest,
1007 ssub,
1008 g.zIpAddr,
1009 suname,
1010 sverified,
1011 zName
1012 );
1013 }else{
1014 db_multi_exec(
1015 "UPDATE subscriber SET"
1016 " sdonotcall=%d,"
1017 " sdigest=%d,"
1018 " ssub=%Q,"
1019 " smtime=julianday('now'),"
1020 " smip=%Q,"
1021 " WHERE subscriberCode=hextoblob(%Q)",
1022 sdonotcall,
1023 sdigest,
1024 ssub,
1025 g.zIpAddr,
1026 zName
1027 );
1028 }
1029 }
1030 if( P("delete")!=0 && cgi_csrf_safe(1) ){
1031 if( !PB("dodelete") ){
1032 eErr = 9;
1033 zErr = mprintf("Select this checkbox and press \"Unsubscribe\" to"
1034 " unsubscribe");
1035 }else{
1036 email_unsubscribe(zName);
1037 return;
1038 }
1039 }
1040 db_prepare(&q,
1041 "SELECT"
1042 " semail,"
1043 " sverified,"
1044 " sdonotcall,"
1045 " sdigest,"
1046 " ssub,"
1047 " smip,"
1048 " suname"
1049 " FROM subscriber WHERE subscriberCode=hextoblob(%Q)", zName);
1050 if( db_step(&q)!=SQLITE_ROW ){
1051 db_finalize(&q);
1052 cgi_redirect("subscribe");
1053 return;
1054 }
1055 style_header("Update Subscription");
1056 semail = db_column_text(&q, 0);
1057 sverified = db_column_int(&q, 1);
1058 sdonotcall = db_column_int(&q, 2);
1059 sdigest = db_column_int(&q, 3);
1060 ssub = db_column_text(&q, 4);
1061 sa = strchr(ssub,'a')!=0;
1062 sc = strchr(ssub,'c')!=0;
1063 st = strchr(ssub,'t')!=0;
1064 sw = strchr(ssub,'w')!=0;
1065 smip = db_column_text(&q, 5);
1066 suname = db_column_text(&q, 6);
1067 if( !g.perm.Admin && !sverified ){
1068 db_multi_exec(
1069 "UPDATE subscriber SET sverified=1 WHERE subscriberCode=hextoblob(%Q)",
1070 zName);
1071 @ <h1>Your email alert subscription has been verified!</h1>
1072 @ <p>Use the form below to update your subscription information.</p>
1073 @ <p>Hint: Bookmark this page so that you can more easily update
1074 @ your subscription information in the future</p>
1075 }else{
1076 @ <p>Make changes to the email subscription shown below and
1077 @ press "Submit".</p>
1078 }
1079 form_begin(0, "%R/alerts");
1080 @ <input type="hidden" name="name" value="%h(zName)">
1081 @ <table class="subscribe">
1082 @ <tr>
1083 @ <td class="form_label">Email&nbsp;Address:</td>
1084 @ <td>%h(semail)</td>
1085 @ </tr>
1086 if( g.perm.Admin ){
1087 @ <tr>
1088 @ <td class='form_label'>IP Address:</td>
1089 @ <td>%h(smip)</td>
1090 @ </tr>
1091 @ <tr>
1092 @ <td class="form_label">User:</td>
1093 @ <td><input type="text" name="suname" value="%h(suname?suname:"")" \
1094 @ size="30"></td>
1095 @ </tr>
1096 }
1097 @ <tr>
1098 @ <td class="form_label">Options:</td>
1099 @ <td><label><input type="checkbox" name="sa" %s(sa?"checked":"")>\
1100 @ Announcements</label><br>
1101 @ <label><input type="checkbox" name="sc" %s(sc?"checked":"")>\
1102 @ Check-ins</label><br>
1103 @ <label><input type="checkbox" name="st" %s(st?"checked":"")>\
1104 @ Ticket changes</label><br>
1105 @ <label><input type="checkbox" name="sw" %s(sw?"checked":"")>\
1106 @ Wiki</label><br>
1107 @ <label><input type="checkbox" name="sdigest" %s(sdigest?"checked":"")>\
1108 @ Daily digest only</label><br>
1109 if( g.perm.Admin ){
1110 @ <label><input type="checkbox" name="sdonotcall" \
1111 @ %s(sdonotcall?"checked":"")> Do not call</label><br>
1112 @ <label><input type="checkbox" name="sverified" \
1113 @ %s(sverified?"checked":"")>\
1114 @ Verified</label><br>
1115 }
1116 @ <label><input type="checkbox" name="dodelete">
1117 @ Unsubscribe</label> \
1118 if( eErr==9 ){
1119 @ <span class="loginError">&larr; %h(zErr)</span>\
1120 }
1121 @ <br>
1122 @ </td></tr>
1123 @ <tr>
1124 @ <td></td>
1125 @ <td><input type="submit" name="submit" value="Submit">
1126 @ <input type="submit" name="delete" value="Unsubscribe">
1127 @ </tr>
1128 @ </table>
1129 @ </form>
1130 fossil_free(zErr);
1131 db_finalize(&q);
1132 style_footer();
1133 }
1134
1135 /* This is the message that gets sent to describe how to change
1136 ** or modify a subscription
1137 */
1138 static const char zUnsubMsg[] =
1139 @ To changes your subscription settings at %s visit this link:
1140 @
1141 @ %s/alerts/%s
1142 @
1143 @ To completely unsubscribe from %s, visit the following link:
1144 @
1145 @ %s/unsubscribe/%s
1146 ;
1147
1148 /*
1149 ** WEBPAGE: unsubscribe
1150 **
1151 ** Users visit this page to be delisted from email alerts.
1152 **
1153 ** If a valid subscriber code is supplied in the name= query parameter,
1154 ** then that subscriber is delisted.
1155 **
1156 ** Otherwise, If the users is logged in, then they are redirected
1157 ** to the /alerts page where they have an unsubscribe button.
1158 **
1159 ** Non-logged-in users with no name= query parameter are invited to enter
1160 ** an email address to which will be sent the unsubscribe link that
1161 ** contains the correct subscriber code.
1162 */
1163 void unsubscribe_page(void){
1164 const char *zName = P("name");
1165 char *zErr = 0;
1166 int eErr = 0;
1167 unsigned int uSeed;
1168 const char *zDecoded;
1169 char *zCaptcha = 0;
1170 int dx;
1171 int bSubmit;
1172 const char *zEAddr;
1173 char *zCode = 0;
1174
1175 /* If a valid subscriber code is supplied, then unsubscribe immediately.
1176 */
1177 if( zName
1178 && db_exists("SELECT 1 FROM subscriber WHERE subscriberCode=hextoblob(%Q)",
1179 zName)
1180 ){
1181 email_unsubscribe(zName);
1182 return;
1183 }
1184
1185 /* Logged in users are redirected to the /alerts page */
1186 login_check_credentials();
1187 if( login_is_individual() ){
1188 cgi_redirectf("%R/alerts");
1189 return;
1190 }
1191
1192 zEAddr = PD("e","");
1193 dx = atoi(PD("dx","0"));
1194 bSubmit = P("submit")!=0 && P("e")!=0 && cgi_csrf_safe(1);
1195 if( bSubmit ){
1196 if( !captcha_is_correct(1) ){
1197 eErr = 2;
1198 zErr = mprintf("enter the security code shown below");
1199 bSubmit = 0;
1200 }
1201 }
1202 if( bSubmit ){
1203 zCode = db_text(0,"SELECT hex(subscriberCode) FROM subscriber"
1204 " WHERE semail=%Q", zEAddr);
1205 if( zCode==0 ){
1206 eErr = 1;
1207 zErr = mprintf("not a valid email address");
1208 bSubmit = 0;
1209 }
1210 }
1211 if( bSubmit ){
1212 /* If we get this far, it means that a valid unsubscribe request has
1213 ** been submitted. Send the appropriate email. */
1214 Blob hdr, body;
1215 blob_init(&hdr,0,0);
1216 blob_init(&body,0,0);
1217 blob_appendf(&hdr, "To: %s\n", zEAddr);
1218 blob_appendf(&hdr, "Subject: Unsubscribe Instructions\n");
1219 blob_appendf(&body, zUnsubMsg/*works-like:"%s%s%s%s%s%s"*/,
1220 g.zBaseURL, g.zBaseURL, zCode, g.zBaseURL, g.zBaseURL, zCode);
1221 email_send(&hdr, &body, 0, 0);
1222 style_header("Unsubscribe Instructions Sent");
1223 @ <p>An email has been sent to "%h(zEAddr)" that explains how to
1224 @ unsubscribe and/or modify your subscription settings</p>
1225 style_footer();
1226 return;
1227 }
1228
1229 /* Non-logged-in users have to enter an email address to which is
1230 ** sent a message containing the unsubscribe link.
1231 */
1232 style_header("Unsubscribe Request");
1233 @ <p>Fill out the form below to request an email message that will
1234 @ explain how to unsubscribe and/or change your subscription settings.</p>
1235 @
1236 form_begin(0, "%R/unsubscribe");
1237 @ <table class="subscribe">
1238 @ <tr>
1239 @ <td class="form_label">Email&nbsp;Address:</td>
1240 @ <td><input type="text" name="e" value="%h(zEAddr)" size="30"></td>
1241 if( eErr==1 ){
1242 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
1243 }
1244 @ </tr>
1245 uSeed = captcha_seed();
1246 zDecoded = captcha_decode(uSeed);
1247 zCaptcha = captcha_render(zDecoded);
1248 @ <tr>
1249 @ <td class="form_label">Security Code:</td>
1250 @ <td><input type="text" name="captcha" value="" size="30">
1251 @ <input type="hidden" name="captchaseed" value="%u(uSeed)"></td>
1252 if( eErr==2 ){
1253 @ <td><span class="loginError">&larr; %h(zErr)</span></td>
1254 }
1255 @ </tr>
1256 @ <tr>
1257 @ <td class="form_label">Options:</td>
1258 @ <td><label><input type="radio" name="dx" value="0" %s(dx?"":"checked")>\
1259 @ Modify subscription</label><br>
1260 @ <label><input type="radio" name="dx" value="1" %s(dx?"checked":"")>\
1261 @ Completely unsubscribe</label><br>
1262 @ <tr>
1263 @ <td></td>
1264 @ <td><input type="submit" name="submit" value="Submit"></td>
1265 @ </tr>
1266 @ </table>
1267 @ <div class="captcha"><table class="captcha"><tr><td><pre>
1268 @ %h(zCaptcha)
1269 @ </pre>
1270 @ Enter the 8 characters above in the "Security Code" box
1271 @ </td></tr></table></div>
1272 @ </form>
1273 fossil_free(zErr);
1274 style_footer();
1275 }
1276
1277 /*
1278 ** WEBPAGE: subscribers
1279 **
1280 ** This page, accessible to administrators only,
1281 ** shows a list of email notification email addresses with
1282 ** links to facilities for editing.
1283 */
1284 void subscriber_list_page(void){
1285 Blob sql;
1286 Stmt q;
1287 login_check_credentials();
1288 if( !g.perm.Admin ){
1289 fossil_redirect_home();
1290 return;
1291 }
1292 email_submenu_common();
1293 style_header("Subscriber List");
1294 blob_init(&sql, 0, 0);
1295 blob_append_sql(&sql,
1296 "SELECT hex(subscriberCode),"
1297 " semail,"
1298 " ssub,"
1299 " suname,"
1300 " sverified,"
1301 " sdigest"
1302 " FROM subscriber"
1303 );
1304 db_prepare_blob(&q, &sql);
1305 @ <table border="1">
1306 @ <tr>
1307 @ <th>Email
1308 @ <th>Events
1309 @ <th>Digest-Only?
1310 @ <th>User
1311 @ <th>Verified?
1312 @ </tr>
1313 while( db_step(&q)==SQLITE_ROW ){
1314 @ <tr>
1315 @ <td><a href='%R/alerts/%s(db_column_text(&q,0))'>\
1316 @ %h(db_column_text(&q,1))</a></td>
1317 @ <td>%h(db_column_text(&q,2))</td>
1318 @ <td>%s(db_column_int(&q,5)?"digest":"")</td>
1319 @ <td>%h(db_column_text(&q,3))</td>
1320 @ <td>%s(db_column_int(&q,4)?"yes":"pending")</td>
1321 @ </tr>
1322 }
1323 @ </table>
1324 db_finalize(&q);
1325 style_footer();
1326 }
1327
1328 #if LOCAL_INTERFACE
1329 /*
1330 ** A single event that might appear in an alert is recorded as an
1331 ** instance of the following object.
1332 */
1333 struct EmailEvent {
1334 int type; /* 'c', 't', 'w', etc. */
1335 Blob txt; /* Text description to appear in an alert */
1336 EmailEvent *pNext; /* Next in chronological order */
1337 };
1338 #endif
1339
1340 /*
1341 ** Free a linked list of EmailEvent objects
1342 */
1343 void email_free_eventlist(EmailEvent *p){
1344 while( p ){
1345 EmailEvent *pNext = p->pNext;
1346 blob_zero(&p->txt);
1347 fossil_free(p);
1348 p = pNext;
1349 }
1350 }
1351
1352 /*
1353 ** Compute and return a linked list of EmailEvent objects
1354 ** corresponding to the current content of the temp.wantalert
1355 ** table which should be defined as follows:
1356 **
1357 ** CREATE TEMP TABLE wantalert(eventId TEXT);
1358 */
1359 EmailEvent *email_compute_event_text(int *pnEvent){
1360 Stmt q;
1361 EmailEvent *p;
1362 EmailEvent anchor;
1363 EmailEvent *pLast;
1364 const char *zUrl = db_get("email-url","http://localhost:8080");
1365
1366 db_prepare(&q,
1367 "SELECT"
1368 " blob.uuid," /* 0 */
1369 " datetime(event.mtime)," /* 1 */
1370 " coalesce(ecomment,comment)"
1371 " || ' (user: ' || coalesce(euser,user,'?')"
1372 " || (SELECT case when length(x)>0 then ' tags: ' || x else '' end"
1373 " FROM (SELECT group_concat(substr(tagname,5), ', ') AS x"
1374 " FROM tag, tagxref"
1375 " WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid"
1376 " AND tagxref.rid=blob.rid AND tagxref.tagtype>0))"
1377 " || ')' as comment," /* 2 */
1378 " tagxref.value AS branch," /* 3 */
1379 " wantalert.eventId" /* 4 */
1380 " FROM temp.wantalert JOIN tag CROSS JOIN event CROSS JOIN blob"
1381 " LEFT JOIN tagxref ON tagxref.tagid=tag.tagid"
1382 " AND tagxref.tagtype>0"
1383 " AND tagxref.rid=blob.rid"
1384 " WHERE blob.rid=event.objid"
1385 " AND tag.tagname='branch'"
1386 " AND event.objid=substr(wantalert.eventId,2)+0"
1387 " ORDER BY event.mtime"
1388 );
1389 memset(&anchor, 0, sizeof(anchor));
1390 pLast = &anchor;
1391 *pnEvent = 0;
1392 while( db_step(&q)==SQLITE_ROW ){
1393 const char *zType = "";
1394 p = fossil_malloc( sizeof(EmailEvent) );
1395 pLast->pNext = p;
1396 pLast = p;
1397 p->type = db_column_text(&q, 4)[0];
1398 p->pNext = 0;
1399 switch( p->type ){
1400 case 'c': zType = "Check-In"; break;
1401 case 't': zType = "Wiki Edit"; break;
1402 case 'w': zType = "Ticket Change"; break;
1403 }
1404 blob_init(&p->txt, 0, 0);
1405 blob_appendf(&p->txt,"== %s %s ==\n%s\n%s/info/%.20s\n",
1406 db_column_text(&q,1),
1407 zType,
1408 db_column_text(&q,2),
1409 zUrl,
1410 db_column_text(&q,0)
1411 );
1412 (*pnEvent)++;
1413 }
1414 db_finalize(&q);
1415 return anchor.pNext;
1416 }
1417
1418 /*
1419 ** Put a header on an alert email
1420 */
1421 void email_header(Blob *pOut){
1422 blob_appendf(pOut,
1423 "This is an automated email reporting changes "
1424 "on Fossil repository %s (%s/timeline)\n",
1425 db_get("email-subname","(unknown)"),
1426 db_get("email-url","http://localhost:8080"));
1427 }
1428
1429 /*
1430 ** Append the "unsubscribe" notification and other footer text to
1431 ** the end of an email alert being assemblied in pOut.
1432 */
1433 void email_footer(Blob *pOut){
1434 blob_appendf(pOut, "\n%.72c\nTo unsubscribe: %s/unsubscribe\n",
1435 '-', db_get("email-url","http://localhost:8080"));
1436 }
1437
1438 /*
1439 ** COMMAND: test-generate-alert
1440 **
1441 ** Usage: %fossil test-generate-alert [--html] [--actual] EVENTID ...
1442 **
1443 ** Generate the text of an email alert for all of the EVENTIDs
1444 ** listed on the command-line. Write that text to standard
1445 ** output. If the --actual flag is present, then the EVENTIDs are
1446 ** the actual event-ids in the pending_alert table.
1447 **
1448 ** This command is intended for testing and debugging the logic
1449 ** that generates email alert text.
1450 */
1451 void test_generate_alert_cmd(void){
1452 int bActual = find_option("actual",0,0)!=0;
1453 Blob out;
1454 int nEvent;
1455 EmailEvent *pEvent, *p;
1456
1457 db_find_and_open_repository(0, 0);
1458 verify_all_options();
1459 db_begin_transaction();
1460 email_schema();
1461 db_multi_exec("CREATE TEMP TABLE wantalert(eventid TEXT)");
1462 if( bActual ){
1463 db_multi_exec("INSERT INTO wantalert SELECT eventid FROM pending_alert");
1464 }else{
1465 int i;
1466 for(i=2; i<g.argc; i++){
1467 db_multi_exec("INSERT INTO wantalert VALUES(%Q)", g.argv[i]);
1468 }
1469 }
1470 blob_init(&out, 0, 0);
1471 email_header(&out);
1472 pEvent = email_compute_event_text(&nEvent);
1473 for(p=pEvent; p; p=p->pNext){
1474 blob_append(&out, "\n", 1);
1475 blob_append(&out, blob_buffer(&p->txt), blob_size(&p->txt));
1476 }
1477 email_free_eventlist(pEvent);
1478 email_footer(&out);
1479 fossil_print("%s", blob_str(&out));
1480 blob_zero(&out);
1481 db_end_transaction(0);
1482 }
1483
1484 /*
1485 ** COMMAND: test-add-alerts
1486 **
1487 ** Usage: %fossil test-add-alerts EVENTID ...
1488 **
1489 ** Add one or more events to the pending_alert queue. Use this
1490 ** command during testing to force email notifications for specific
1491 ** events.
1492 */
1493 void test_add_alert_cmd(void){
1494 int i;
1495 db_find_and_open_repository(0, 0);
1496 verify_all_options();
1497 db_begin_transaction();
1498 email_schema();
1499 for(i=2; i<g.argc; i++){
1500 db_multi_exec("INSERT INTO pending_alert(eventId) VALUES(%Q)", g.argv[i]);
1501 }
1502 db_end_transaction(0);
1503 }
1504
1505 #if INTERFACE
1506 /*
1507 ** Flags for email_send_alerts()
1508 */
1509 #define SENDALERT_DIGEST 0x0001 /* Send a digest */
1510 #define SENDALERT_PRESERVE 0x0002 /* Do not mark the task as done */
1511
1512 #endif /* INTERFACE */
1513
1514 /*
1515 ** Send alert emails to all subscribers
1516 */
1517 void email_send_alerts(u32 flags){
1518 EmailEvent *pEvents, *p;
1519 int nEvent = 0;
1520 Stmt q;
1521 const char *zDigest = "false";
1522 Blob hdr, body;
1523 const char *zUrl;
1524 const char *zRepoName;
1525 const char *zFrom;
1526
1527 db_begin_transaction();
1528 if( !email_enabled() ) goto send_alerts_done;
1529 zUrl = db_get("email-url",0);
1530 if( zUrl==0 ) goto send_alerts_done;
1531 zRepoName = db_get("email-subname",0);
1532 if( zRepoName==0 ) goto send_alerts_done;
1533 zFrom = db_get("email-self",0);
1534 if( zFrom==0 ) goto send_alerts_done;
1535 db_multi_exec(
1536 "DROP TABLE IF EXISTS temp.wantalert;"
1537 "CREATE TEMP TABLE wantalert(eventId TEXT);"
1538 );
1539 if( flags & SENDALERT_DIGEST ){
1540 db_multi_exec(
1541 "INSERT INTO wantalert SELECT eventid FROM pending_alert"
1542 " WHERE sentDigest IS FALSE"
1543 );
1544 zDigest = "true";
1545 }else{
1546 db_multi_exec(
1547 "INSERT INTO wantalert SELECT eventid FROM pending_alert"
1548 " WHERE sentSep IS FALSE"
1549 );
1550 }
1551 pEvents = email_compute_event_text(&nEvent);
1552 if( nEvent==0 ) return;
1553 blob_init(&hdr, 0, 0);
1554 blob_init(&body, 0, 0);
1555 db_prepare(&q,
1556 "SELECT"
1557 " subscriberCode," /* 0 */
1558 " semail," /* 1 */
1559 " ssub" /* 2 */
1560 " FROM subscriber"
1561 " WHERE sverified AND NOT sdonotcall"
1562 " AND sdigest IS %s",
1563 zDigest/*safe-for-%s*/
1564 );
1565 while( db_step(&q)==SQLITE_ROW ){
1566 const char *zCode = db_column_text(&q, 0);
1567 const char *zSub = db_column_text(&q, 2);
1568 const char *zEmail = db_column_text(&q, 1);
1569 int nHit = 0;
1570 for(p=pEvents; p; p=p->pNext){
1571 if( strchr(zSub,p->type)==0 ) continue;
1572 if( nHit==0 ){
1573 blob_appendf(&hdr,"To: %s\n", zEmail);
1574 blob_appendf(&hdr,"From: %s\n", zFrom);
1575 blob_appendf(&hdr,"Subject: %s activity alert\n", zRepoName);
1576 blob_appendf(&body,
1577 "This is an automated email sent by the Fossil repository "
1578 "at %s to alert you to changes.\n",
1579 zUrl
1580 );
1581 }
1582 nHit++;
1583 blob_append(&body, "\n", 1);
1584 blob_append(&body, blob_buffer(&p->txt), blob_size(&p->txt));
1585 }
1586 if( nHit==0 ) continue;
1587 blob_appendf(&body,"\n%.72c\nSubscription info: %s/alerts/%s\n",
1588 '-', zUrl, zCode);
1589 email_send(&hdr,&body,0,0);
1590 blob_truncate(&hdr);
1591 blob_truncate(&body);
1592 }
1593 blob_zero(&hdr);
1594 blob_zero(&body);
1595 db_finalize(&q);
1596 email_free_eventlist(pEvents);
1597 if( (flags & SENDALERT_PRESERVE)==0 ){
1598 if( flags & SENDALERT_DIGEST ){
1599 db_multi_exec("UPDATE pending_alert SET sentDigest=true");
1600 }else{
1601 db_multi_exec("UPDATE pending_alert SET sentSep=true");
1602 }
1603 db_multi_exec("DELETE FROM pending_alert WHERE sentDigest AND sentSep");
1604 }
1605 send_alerts_done:
1606 db_end_transaction(0);
1607 }
1608
--- src/encode.c
+++ src/encode.c
@@ -634,10 +634,11 @@
634634
** Return true if the input string contains only valid base-16 digits.
635635
** If any invalid characters appear in the string, return false.
636636
*/
637637
int validate16(const char *zIn, int nIn){
638638
int i;
639
+ if( nIn<0 ) nIn = (int)strlen(zIn);
639640
for(i=0; i<nIn; i++, zIn++){
640641
if( zDecode[zIn[0]&0xff]>63 ){
641642
return zIn[0]==0;
642643
}
643644
}
644645
--- src/encode.c
+++ src/encode.c
@@ -634,10 +634,11 @@
634 ** Return true if the input string contains only valid base-16 digits.
635 ** If any invalid characters appear in the string, return false.
636 */
637 int validate16(const char *zIn, int nIn){
638 int i;
 
639 for(i=0; i<nIn; i++, zIn++){
640 if( zDecode[zIn[0]&0xff]>63 ){
641 return zIn[0]==0;
642 }
643 }
644
--- src/encode.c
+++ src/encode.c
@@ -634,10 +634,11 @@
634 ** Return true if the input string contains only valid base-16 digits.
635 ** If any invalid characters appear in the string, return false.
636 */
637 int validate16(const char *zIn, int nIn){
638 int i;
639 if( nIn<0 ) nIn = (int)strlen(zIn);
640 for(i=0; i<nIn; i++, zIn++){
641 if( zDecode[zIn[0]&0xff]>63 ){
642 return zIn[0]==0;
643 }
644 }
645
--- src/forum.c
+++ src/forum.c
@@ -101,13 +101,11 @@
101101
itemId
102102
);
103103
while( db_step(&q)==SQLITE_ROW ){
104104
int id = db_column_int(&q, 0);
105105
const char *zUser = db_column_text(&q, 1);
106
- const char *zStat = db_column_text(&q, 2);
107106
const char *zMime = db_column_text(&q, 3);
108
- const char *zIp = db_column_text(&q, 4);
109107
int iDepth = db_column_int(&q, 7);
110108
double rMTime = db_column_double(&q, 8);
111109
char *zAge = db_timespan_name(rNow - rMTime);
112110
Blob body;
113111
@ <!-- Forum post %d(id) -->
@@ -257,11 +255,10 @@
257255
void forum_edit_page(void){
258256
int itemId;
259257
int parentId;
260258
char *zErr = 0;
261259
login_check_credentials();
262
- const char *zBody;
263260
const char *zMime;
264261
const char *zSub;
265262
if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
266263
forum_verify_schema();
267264
itemId = atoi(PD("item","0"));
268265
--- src/forum.c
+++ src/forum.c
@@ -101,13 +101,11 @@
101 itemId
102 );
103 while( db_step(&q)==SQLITE_ROW ){
104 int id = db_column_int(&q, 0);
105 const char *zUser = db_column_text(&q, 1);
106 const char *zStat = db_column_text(&q, 2);
107 const char *zMime = db_column_text(&q, 3);
108 const char *zIp = db_column_text(&q, 4);
109 int iDepth = db_column_int(&q, 7);
110 double rMTime = db_column_double(&q, 8);
111 char *zAge = db_timespan_name(rNow - rMTime);
112 Blob body;
113 @ <!-- Forum post %d(id) -->
@@ -257,11 +255,10 @@
257 void forum_edit_page(void){
258 int itemId;
259 int parentId;
260 char *zErr = 0;
261 login_check_credentials();
262 const char *zBody;
263 const char *zMime;
264 const char *zSub;
265 if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
266 forum_verify_schema();
267 itemId = atoi(PD("item","0"));
268
--- src/forum.c
+++ src/forum.c
@@ -101,13 +101,11 @@
101 itemId
102 );
103 while( db_step(&q)==SQLITE_ROW ){
104 int id = db_column_int(&q, 0);
105 const char *zUser = db_column_text(&q, 1);
 
106 const char *zMime = db_column_text(&q, 3);
 
107 int iDepth = db_column_int(&q, 7);
108 double rMTime = db_column_double(&q, 8);
109 char *zAge = db_timespan_name(rNow - rMTime);
110 Blob body;
111 @ <!-- Forum post %d(id) -->
@@ -257,11 +255,10 @@
255 void forum_edit_page(void){
256 int itemId;
257 int parentId;
258 char *zErr = 0;
259 login_check_credentials();
 
260 const char *zMime;
261 const char *zSub;
262 if( !g.perm.WrForum ){ login_needed(g.anon.WrForum); return; }
263 forum_verify_schema();
264 itemId = atoi(PD("item","0"));
265
+14
--- src/login.c
+++ src/login.c
@@ -730,10 +730,15 @@
730730
free(zCaptcha);
731731
}
732732
@ </form>
733733
}
734734
if( g.zLogin && g.perm.Password ){
735
+ if( email_enabled() ){
736
+ @ <hr>
737
+ @ <p>Configure <a href="%R/alerts">Email Alerts</a>
738
+ @ for user <b>%h(g.zLogin)</b></p>
739
+ }
735740
@ <hr />
736741
@ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
737742
form_begin(0, "%R/login");
738743
@ <table>
739744
@ <tr><td class="form_label">Old Password:</td>
@@ -1348,10 +1353,19 @@
13481353
** Return true if the user is "nobody"
13491354
*/
13501355
int login_is_nobody(void){
13511356
return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0;
13521357
}
1358
+
1359
+/*
1360
+** Return true if the user is a specific individual, not "nobody" or
1361
+** "anonymous".
1362
+*/
1363
+int login_is_individual(void){
1364
+ return g.zLogin!=0 && g.zLogin[0]!=0 && fossil_strcmp(g.zLogin,"nobody")!=0
1365
+ && fossil_strcmp(g.zLogin,"anonymous")!=0;
1366
+}
13531367
13541368
/*
13551369
** Return the login name. If no login name is specified, return "nobody".
13561370
*/
13571371
const char *login_name(void){
13581372
--- src/login.c
+++ src/login.c
@@ -730,10 +730,15 @@
730 free(zCaptcha);
731 }
732 @ </form>
733 }
734 if( g.zLogin && g.perm.Password ){
 
 
 
 
 
735 @ <hr />
736 @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
737 form_begin(0, "%R/login");
738 @ <table>
739 @ <tr><td class="form_label">Old Password:</td>
@@ -1348,10 +1353,19 @@
1348 ** Return true if the user is "nobody"
1349 */
1350 int login_is_nobody(void){
1351 return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0;
1352 }
 
 
 
 
 
 
 
 
 
1353
1354 /*
1355 ** Return the login name. If no login name is specified, return "nobody".
1356 */
1357 const char *login_name(void){
1358
--- src/login.c
+++ src/login.c
@@ -730,10 +730,15 @@
730 free(zCaptcha);
731 }
732 @ </form>
733 }
734 if( g.zLogin && g.perm.Password ){
735 if( email_enabled() ){
736 @ <hr>
737 @ <p>Configure <a href="%R/alerts">Email Alerts</a>
738 @ for user <b>%h(g.zLogin)</b></p>
739 }
740 @ <hr />
741 @ <p>Change Password for user <b>%h(g.zLogin)</b>:</p>
742 form_begin(0, "%R/login");
743 @ <table>
744 @ <tr><td class="form_label">Old Password:</td>
@@ -1348,10 +1353,19 @@
1353 ** Return true if the user is "nobody"
1354 */
1355 int login_is_nobody(void){
1356 return g.zLogin==0 || g.zLogin[0]==0 || fossil_strcmp(g.zLogin,"nobody")==0;
1357 }
1358
1359 /*
1360 ** Return true if the user is a specific individual, not "nobody" or
1361 ** "anonymous".
1362 */
1363 int login_is_individual(void){
1364 return g.zLogin!=0 && g.zLogin[0]!=0 && fossil_strcmp(g.zLogin,"nobody")!=0
1365 && fossil_strcmp(g.zLogin,"anonymous")!=0;
1366 }
1367
1368 /*
1369 ** Return the login name. If no login name is specified, return "nobody".
1370 */
1371 const char *login_name(void){
1372
+6 -2
--- src/rebuild.c
+++ src/rebuild.c
@@ -144,11 +144,12 @@
144144
** Update the repository schema for Fossil version 2.0. (2017-02-28)
145145
** (1) Change the CHECK constraint on BLOB.UUID so that the length
146146
** is greater than or equal to 40, not exactly equal to 40.
147147
*/
148148
void rebuild_schema_update_2_0(void){
149
- char *z = db_text(0, "SELECT sql FROM repository.sqlite_master WHERE name='blob'");
149
+ char *z = db_text(0, "SELECT sql FROM repository.sqlite_master"
150
+ " WHERE name='blob'");
150151
if( z ){
151152
/* Search for: length(uuid)==40
152153
** 0123456789 12345 */
153154
int i;
154155
for(i=10; z[i]; i++){
@@ -357,19 +358,21 @@
357358
ttyOutput = doOut;
358359
processCnt = 0;
359360
if (ttyOutput && !g.fQuiet) {
360361
percent_complete(0);
361362
}
363
+ email_triggers_disable();
362364
rebuild_update_schema();
363365
blob_init(&sql, 0, 0);
364366
db_prepare(&q,
365367
"SELECT name FROM sqlite_master /*scan*/"
366368
" WHERE type='table'"
367369
" AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias',"
368370
"'config','shun','private','reportfmt',"
369371
"'concealed','accesslog','modreq',"
370
- "'purgeevent','purgeitem','unversioned')"
372
+ "'purgeevent','purgeitem','unversioned',"
373
+ "'subscriber','pending_alert','email_bounce')"
371374
" AND name NOT GLOB 'sqlite_*'"
372375
" AND name NOT GLOB 'fx_*'"
373376
);
374377
while( db_step(&q)==SQLITE_ROW ){
375378
blob_appendf(&sql, "DROP TABLE IF EXISTS \"%w\";\n", db_column_text(&q,0));
@@ -446,10 +449,11 @@
446449
if( doClustering ) create_cluster();
447450
if( ttyOutput && !g.fQuiet && totalSize>0 ){
448451
processCnt += incrSize;
449452
percent_complete((processCnt*1000)/totalSize);
450453
}
454
+ email_triggers_enable();
451455
if(!g.fQuiet && ttyOutput ){
452456
percent_complete(1000);
453457
fossil_print("\n");
454458
}
455459
return errCnt;
456460
--- src/rebuild.c
+++ src/rebuild.c
@@ -144,11 +144,12 @@
144 ** Update the repository schema for Fossil version 2.0. (2017-02-28)
145 ** (1) Change the CHECK constraint on BLOB.UUID so that the length
146 ** is greater than or equal to 40, not exactly equal to 40.
147 */
148 void rebuild_schema_update_2_0(void){
149 char *z = db_text(0, "SELECT sql FROM repository.sqlite_master WHERE name='blob'");
 
150 if( z ){
151 /* Search for: length(uuid)==40
152 ** 0123456789 12345 */
153 int i;
154 for(i=10; z[i]; i++){
@@ -357,19 +358,21 @@
357 ttyOutput = doOut;
358 processCnt = 0;
359 if (ttyOutput && !g.fQuiet) {
360 percent_complete(0);
361 }
 
362 rebuild_update_schema();
363 blob_init(&sql, 0, 0);
364 db_prepare(&q,
365 "SELECT name FROM sqlite_master /*scan*/"
366 " WHERE type='table'"
367 " AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias',"
368 "'config','shun','private','reportfmt',"
369 "'concealed','accesslog','modreq',"
370 "'purgeevent','purgeitem','unversioned')"
 
371 " AND name NOT GLOB 'sqlite_*'"
372 " AND name NOT GLOB 'fx_*'"
373 );
374 while( db_step(&q)==SQLITE_ROW ){
375 blob_appendf(&sql, "DROP TABLE IF EXISTS \"%w\";\n", db_column_text(&q,0));
@@ -446,10 +449,11 @@
446 if( doClustering ) create_cluster();
447 if( ttyOutput && !g.fQuiet && totalSize>0 ){
448 processCnt += incrSize;
449 percent_complete((processCnt*1000)/totalSize);
450 }
 
451 if(!g.fQuiet && ttyOutput ){
452 percent_complete(1000);
453 fossil_print("\n");
454 }
455 return errCnt;
456
--- src/rebuild.c
+++ src/rebuild.c
@@ -144,11 +144,12 @@
144 ** Update the repository schema for Fossil version 2.0. (2017-02-28)
145 ** (1) Change the CHECK constraint on BLOB.UUID so that the length
146 ** is greater than or equal to 40, not exactly equal to 40.
147 */
148 void rebuild_schema_update_2_0(void){
149 char *z = db_text(0, "SELECT sql FROM repository.sqlite_master"
150 " WHERE name='blob'");
151 if( z ){
152 /* Search for: length(uuid)==40
153 ** 0123456789 12345 */
154 int i;
155 for(i=10; z[i]; i++){
@@ -357,19 +358,21 @@
358 ttyOutput = doOut;
359 processCnt = 0;
360 if (ttyOutput && !g.fQuiet) {
361 percent_complete(0);
362 }
363 email_triggers_disable();
364 rebuild_update_schema();
365 blob_init(&sql, 0, 0);
366 db_prepare(&q,
367 "SELECT name FROM sqlite_master /*scan*/"
368 " WHERE type='table'"
369 " AND name NOT IN ('admin_log', 'blob','delta','rcvfrom','user','alias',"
370 "'config','shun','private','reportfmt',"
371 "'concealed','accesslog','modreq',"
372 "'purgeevent','purgeitem','unversioned',"
373 "'subscriber','pending_alert','email_bounce')"
374 " AND name NOT GLOB 'sqlite_*'"
375 " AND name NOT GLOB 'fx_*'"
376 );
377 while( db_step(&q)==SQLITE_ROW ){
378 blob_appendf(&sql, "DROP TABLE IF EXISTS \"%w\";\n", db_column_text(&q,0));
@@ -446,10 +449,11 @@
449 if( doClustering ) create_cluster();
450 if( ttyOutput && !g.fQuiet && totalSize>0 ){
451 processCnt += incrSize;
452 percent_complete((processCnt*1000)/totalSize);
453 }
454 email_triggers_enable();
455 if(!g.fQuiet && ttyOutput ){
456 percent_complete(1000);
457 fossil_print("\n");
458 }
459 return errCnt;
460
+2 -2
--- src/setup.c
+++ src/setup.c
@@ -1022,13 +1022,13 @@
10221022
admin_log("Set entry_attribute %Q to: %.*s%s",
10231023
zVar, 20, zQ, (nZQ>20 ? "..." : ""));
10241024
zVal = zQ;
10251025
}
10261026
@ <input type="text" id="%s(zQParm)" name="%s(zQParm)" value="%h(zVal)" \
1027
- @ size="%d(width)"
1027
+ @ size="%d(width)" \
10281028
if( disabled ){
1029
- @ disabled="disabled"
1029
+ @ disabled="disabled" \
10301030
}
10311031
@ /> <b>%s(zLabel)</b>
10321032
}
10331033
10341034
/*
10351035
--- src/setup.c
+++ src/setup.c
@@ -1022,13 +1022,13 @@
1022 admin_log("Set entry_attribute %Q to: %.*s%s",
1023 zVar, 20, zQ, (nZQ>20 ? "..." : ""));
1024 zVal = zQ;
1025 }
1026 @ <input type="text" id="%s(zQParm)" name="%s(zQParm)" value="%h(zVal)" \
1027 @ size="%d(width)"
1028 if( disabled ){
1029 @ disabled="disabled"
1030 }
1031 @ /> <b>%s(zLabel)</b>
1032 }
1033
1034 /*
1035
--- src/setup.c
+++ src/setup.c
@@ -1022,13 +1022,13 @@
1022 admin_log("Set entry_attribute %Q to: %.*s%s",
1023 zVar, 20, zQ, (nZQ>20 ? "..." : ""));
1024 zVal = zQ;
1025 }
1026 @ <input type="text" id="%s(zQParm)" name="%s(zQParm)" value="%h(zVal)" \
1027 @ size="%d(width)" \
1028 if( disabled ){
1029 @ disabled="disabled" \
1030 }
1031 @ /> <b>%s(zLabel)</b>
1032 }
1033
1034 /*
1035
+7 -1
--- src/timeline.c
+++ src/timeline.c
@@ -2336,10 +2336,11 @@
23362336
** N is negative, output the first -N lines. If N is
23372337
** zero, no limit. Default is -20 meaning 20 lines.
23382338
** -p|--path PATH Output items affecting PATH only.
23392339
** PATH can be a file or a sub directory.
23402340
** --offset P skip P changes
2341
+** --sql Show the SQL used to generate the timeline
23412342
** -t|--type TYPE Output items from the given types only, such as:
23422343
** ci = file commits only
23432344
** e = technical notes only
23442345
** t = tickets only
23452346
** w = wiki commits only
@@ -2367,10 +2368,11 @@
23672368
int mode = TIMELINE_MODE_NONE;
23682369
int verboseFlag = 0 ;
23692370
int iOffset;
23702371
const char *zFilePattern = 0;
23712372
Blob treeName;
2373
+ int showSql = 0;
23722374
23732375
verboseFlag = find_option("verbose","v", 0)!=0;
23742376
if( !verboseFlag){
23752377
verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */
23762378
}
@@ -2377,10 +2379,11 @@
23772379
db_find_and_open_repository(0, 0);
23782380
zLimit = find_option("limit","n",1);
23792381
zWidth = find_option("width","W",1);
23802382
zType = find_option("type","t",1);
23812383
zFilePattern = find_option("path","p",1);
2384
+ showSql = find_option("sql",0,0)!=0;
23822385
23832386
if( !zLimit ){
23842387
zLimit = find_option("count",0,1);
23852388
}
23862389
if( zLimit ){
@@ -2520,11 +2523,14 @@
25202523
if( iOffset>0 ){
25212524
/* Don't handle LIMIT here, otherwise print_timeline()
25222525
* will not determine the end-marker correctly! */
25232526
blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
25242527
}
2525
- db_prepare(&q, "%s", blob_sql_text(&sql));
2528
+ if( showSql ){
2529
+ fossil_print("%s\n", blob_str(&sql));
2530
+ }
2531
+ db_prepare_blob(&q, &sql);
25262532
blob_reset(&sql);
25272533
print_timeline(&q, n, width, verboseFlag);
25282534
db_finalize(&q);
25292535
}
25302536
25312537
--- src/timeline.c
+++ src/timeline.c
@@ -2336,10 +2336,11 @@
2336 ** N is negative, output the first -N lines. If N is
2337 ** zero, no limit. Default is -20 meaning 20 lines.
2338 ** -p|--path PATH Output items affecting PATH only.
2339 ** PATH can be a file or a sub directory.
2340 ** --offset P skip P changes
 
2341 ** -t|--type TYPE Output items from the given types only, such as:
2342 ** ci = file commits only
2343 ** e = technical notes only
2344 ** t = tickets only
2345 ** w = wiki commits only
@@ -2367,10 +2368,11 @@
2367 int mode = TIMELINE_MODE_NONE;
2368 int verboseFlag = 0 ;
2369 int iOffset;
2370 const char *zFilePattern = 0;
2371 Blob treeName;
 
2372
2373 verboseFlag = find_option("verbose","v", 0)!=0;
2374 if( !verboseFlag){
2375 verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */
2376 }
@@ -2377,10 +2379,11 @@
2377 db_find_and_open_repository(0, 0);
2378 zLimit = find_option("limit","n",1);
2379 zWidth = find_option("width","W",1);
2380 zType = find_option("type","t",1);
2381 zFilePattern = find_option("path","p",1);
 
2382
2383 if( !zLimit ){
2384 zLimit = find_option("count",0,1);
2385 }
2386 if( zLimit ){
@@ -2520,11 +2523,14 @@
2520 if( iOffset>0 ){
2521 /* Don't handle LIMIT here, otherwise print_timeline()
2522 * will not determine the end-marker correctly! */
2523 blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
2524 }
2525 db_prepare(&q, "%s", blob_sql_text(&sql));
 
 
 
2526 blob_reset(&sql);
2527 print_timeline(&q, n, width, verboseFlag);
2528 db_finalize(&q);
2529 }
2530
2531
--- src/timeline.c
+++ src/timeline.c
@@ -2336,10 +2336,11 @@
2336 ** N is negative, output the first -N lines. If N is
2337 ** zero, no limit. Default is -20 meaning 20 lines.
2338 ** -p|--path PATH Output items affecting PATH only.
2339 ** PATH can be a file or a sub directory.
2340 ** --offset P skip P changes
2341 ** --sql Show the SQL used to generate the timeline
2342 ** -t|--type TYPE Output items from the given types only, such as:
2343 ** ci = file commits only
2344 ** e = technical notes only
2345 ** t = tickets only
2346 ** w = wiki commits only
@@ -2367,10 +2368,11 @@
2368 int mode = TIMELINE_MODE_NONE;
2369 int verboseFlag = 0 ;
2370 int iOffset;
2371 const char *zFilePattern = 0;
2372 Blob treeName;
2373 int showSql = 0;
2374
2375 verboseFlag = find_option("verbose","v", 0)!=0;
2376 if( !verboseFlag){
2377 verboseFlag = find_option("showfiles","f", 0)!=0; /* deprecated */
2378 }
@@ -2377,10 +2379,11 @@
2379 db_find_and_open_repository(0, 0);
2380 zLimit = find_option("limit","n",1);
2381 zWidth = find_option("width","W",1);
2382 zType = find_option("type","t",1);
2383 zFilePattern = find_option("path","p",1);
2384 showSql = find_option("sql",0,0)!=0;
2385
2386 if( !zLimit ){
2387 zLimit = find_option("count",0,1);
2388 }
2389 if( zLimit ){
@@ -2520,11 +2523,14 @@
2523 if( iOffset>0 ){
2524 /* Don't handle LIMIT here, otherwise print_timeline()
2525 * will not determine the end-marker correctly! */
2526 blob_append_sql(&sql, "\n LIMIT -1 OFFSET %d", iOffset);
2527 }
2528 if( showSql ){
2529 fossil_print("%s\n", blob_str(&sql));
2530 }
2531 db_prepare_blob(&q, &sql);
2532 blob_reset(&sql);
2533 print_timeline(&q, n, width, verboseFlag);
2534 db_finalize(&q);
2535 }
2536
2537
+1 -1
--- src/tkt.c
+++ src/tkt.c
@@ -594,11 +594,11 @@
594594
int nJ = 0;
595595
Blob tktchng, cksum;
596596
int needMod;
597597
598598
login_verify_csrf_secret();
599
- if( !captcha_is_correct() ){
599
+ if( !captcha_is_correct(0) ){
600600
@ <p class="generalError">Error: Incorrect security code.</p>
601601
return TH_OK;
602602
}
603603
zUuid = (const char *)pUuid;
604604
blob_zero(&tktchng);
605605
--- src/tkt.c
+++ src/tkt.c
@@ -594,11 +594,11 @@
594 int nJ = 0;
595 Blob tktchng, cksum;
596 int needMod;
597
598 login_verify_csrf_secret();
599 if( !captcha_is_correct() ){
600 @ <p class="generalError">Error: Incorrect security code.</p>
601 return TH_OK;
602 }
603 zUuid = (const char *)pUuid;
604 blob_zero(&tktchng);
605
--- src/tkt.c
+++ src/tkt.c
@@ -594,11 +594,11 @@
594 int nJ = 0;
595 Blob tktchng, cksum;
596 int needMod;
597
598 login_verify_csrf_secret();
599 if( !captcha_is_correct(0) ){
600 @ <p class="generalError">Error: Incorrect security code.</p>
601 return TH_OK;
602 }
603 zUuid = (const char *)pUuid;
604 blob_zero(&tktchng);
605
+2 -2
--- src/wiki.c
+++ src/wiki.c
@@ -534,11 +534,11 @@
534534
zBody = pWiki->zWiki;
535535
zMimetype = pWiki->zMimetype;
536536
}
537537
}
538538
if( P("submit")!=0 && zBody!=0
539
- && (goodCaptcha = captcha_is_correct())
539
+ && (goodCaptcha = captcha_is_correct(0))
540540
){
541541
char *zDate;
542542
Blob cksum;
543543
blob_zero(&wiki);
544544
db_begin_transaction();
@@ -758,11 +758,11 @@
758758
if( !g.perm.ApndWiki ){
759759
login_needed(g.anon.ApndWiki);
760760
return;
761761
}
762762
if( P("submit")!=0 && P("r")!=0 && P("u")!=0
763
- && (goodCaptcha = captcha_is_correct())
763
+ && (goodCaptcha = captcha_is_correct(0))
764764
){
765765
char *zDate;
766766
Blob cksum;
767767
Blob body;
768768
Blob wiki;
769769
--- src/wiki.c
+++ src/wiki.c
@@ -534,11 +534,11 @@
534 zBody = pWiki->zWiki;
535 zMimetype = pWiki->zMimetype;
536 }
537 }
538 if( P("submit")!=0 && zBody!=0
539 && (goodCaptcha = captcha_is_correct())
540 ){
541 char *zDate;
542 Blob cksum;
543 blob_zero(&wiki);
544 db_begin_transaction();
@@ -758,11 +758,11 @@
758 if( !g.perm.ApndWiki ){
759 login_needed(g.anon.ApndWiki);
760 return;
761 }
762 if( P("submit")!=0 && P("r")!=0 && P("u")!=0
763 && (goodCaptcha = captcha_is_correct())
764 ){
765 char *zDate;
766 Blob cksum;
767 Blob body;
768 Blob wiki;
769
--- src/wiki.c
+++ src/wiki.c
@@ -534,11 +534,11 @@
534 zBody = pWiki->zWiki;
535 zMimetype = pWiki->zMimetype;
536 }
537 }
538 if( P("submit")!=0 && zBody!=0
539 && (goodCaptcha = captcha_is_correct(0))
540 ){
541 char *zDate;
542 Blob cksum;
543 blob_zero(&wiki);
544 db_begin_transaction();
@@ -758,11 +758,11 @@
758 if( !g.perm.ApndWiki ){
759 login_needed(g.anon.ApndWiki);
760 return;
761 }
762 if( P("submit")!=0 && P("r")!=0 && P("u")!=0
763 && (goodCaptcha = captcha_is_correct(0))
764 ){
765 char *zDate;
766 Blob cksum;
767 Blob body;
768 Blob wiki;
769

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button