Fossil SCM

Finish off webmail delete

danshearer 2021-03-16 20:10 trunk
Commit 274df1cad42f304befd5a0926c937c2e2bb322af54a985e397f180b37ad13ff3
1 file changed -921
D src/webmail.c
-921
--- a/src/webmail.c
+++ b/src/webmail.c
@@ -1,921 +0,0 @@
1
-/*
2
-** Copyright (c) 2018 D. Richard Hipp
3
-**
4
-** This program is free software; you can redistribute it and/or
5
-** modify it under the terms of the Simplified BSD License (also
6
-** known as the "2-Clause License" or "FreeBSD License".)
7
-**
8
-** This program is distributed in the hope that it will be useful,
9
-** but without any warranty; without even the implied warranty of
10
-** merchantability or fitness for a particular purpose.
11
-**
12
-** Author contact information:
13
-** [email protected]
14
-** http://www.hwaci.com/drh/
15
-**
16
-*******************************************************************************
17
-**
18
-** Implementation of web pages for managing the email storage tables
19
-** (if they exist):
20
-**
21
-** emailbox
22
-** emailblob
23
-** emailroute
24
-*/
25
-#include "config.h"
26
-#include "webmail.h"
27
-#include <assert.h>
28
-
29
-
30
-#if INTERFACE
31
-
32
-/* Recognized content encodings */
33
-#define EMAILENC_NONE 0 /* No encoding */
34
-#define EMAILENC_B64 1 /* Base64 encoded */
35
-#define EMAILENC_QUOTED 2 /* Quoted printable */
36
-
37
-/* An instance of the following object records the location of important
38
-** attributes on a single element in a multipart email message body.
39
-*/
40
-struct EmailBody {
41
- char zMimetype[32]; /* Mimetype */
42
- u8 encoding; /* Type of encoding */
43
- char *zFilename; /* From content-disposition: */
44
- char *zContent; /* Content. \0 terminator inserted */
45
-};
46
-
47
-/*
48
-** An instance of the following object describes the struture of
49
-** an rfc-2822 email message.
50
-*/
51
-struct EmailToc {
52
- int nHdr; /* Number of header lines */
53
- int nHdrAlloc; /* Number of header lines allocated */
54
- char **azHdr; /* Pointer to header line. \0 terminator inserted */
55
- int nBody; /* Number of body segments */
56
- int nBodyAlloc; /* Number of body segments allocated */
57
- EmailBody *aBody; /* Location of body information */
58
-};
59
-#endif
60
-
61
-/*
62
-** Free An EmailToc object
63
-*/
64
-void emailtoc_free(EmailToc *p){
65
- int i;
66
- fossil_free(p->azHdr);
67
- for(i=0; i<p->nBody; i++){
68
- fossil_free(p->aBody[i].zFilename);
69
- }
70
- fossil_free(p->aBody);
71
- fossil_free(p);
72
-}
73
-
74
-/*
75
-** Allocate a new EmailToc object
76
-*/
77
-EmailToc *emailtoc_alloc(void){
78
- EmailToc *p = fossil_malloc( sizeof(*p) );
79
- memset(p, 0, sizeof(*p));
80
- return p;
81
-}
82
-
83
-/*
84
-** Add a new body element to an EmailToc.
85
-*/
86
-EmailBody *emailtoc_new_body(EmailToc *p){
87
- EmailBody *pNew;
88
- p->nBody++;
89
- if( p->nBody>p->nBodyAlloc ){
90
- p->nBodyAlloc = (p->nBodyAlloc+1)*2;
91
- p->aBody = fossil_realloc(p->aBody, sizeof(p->aBody[0])*p->nBodyAlloc);
92
- }
93
- pNew = &p->aBody[p->nBody-1];
94
- memset(pNew, 0, sizeof(*pNew));
95
- return pNew;
96
-}
97
-
98
-/*
99
-** Add a new header line to the EmailToc.
100
-*/
101
-void emailtoc_new_header_line(EmailToc *p, char *z){
102
- p->nHdr++;
103
- if( p->nHdr>p->nHdrAlloc ){
104
- p->nHdrAlloc = (p->nHdrAlloc+1)*2;
105
- p->azHdr = fossil_realloc(p->azHdr, sizeof(p->azHdr[0])*p->nHdrAlloc);
106
- }
107
- p->azHdr[p->nHdr-1] = z;
108
-}
109
-
110
-/*
111
-** Return the length of a line in an email header. Continuation lines
112
-** are included. Hence, this routine returns the number of bytes up to
113
-** and including the first \n character that is followed by something
114
-** other than whitespace.
115
-*/
116
-static int email_line_length(const char *z){
117
- int i;
118
- for(i=0; z[i] && (z[i]!='\n' || z[i+1]==' ' || z[i+1]=='\t'); i++){}
119
- if( z[i]=='\n' ) i++;
120
- return i;
121
-}
122
-
123
-/*
124
-** Look for a parameter of the form NAME=VALUE in the given email
125
-** header line. Return a copy of VALUE in space obtained from
126
-** fossil_malloc(). Or return NULL if there is no such parameter.
127
-*/
128
-static char *email_hdr_value(const char *z, const char *zName){
129
- int nName = (int)strlen(zName);
130
- int i;
131
- const char *z2 = strstr(z, zName);
132
- if( z2==0 ) return 0;
133
- z2 += nName;
134
- if( z2[0]!='=' ) return 0;
135
- z2++;
136
- if( z2[0]=='"' ){
137
- z2++;
138
- for(i=0; z2[i] && z2[i]!='"'; i++){}
139
- if( z2[i]!='"' ) return 0;
140
- }else{
141
- for(i=0; z2[i] && !fossil_isspace(z2[i]); i++){}
142
- }
143
- return mprintf("%.*s", i, z2);
144
-}
145
-
146
-/*
147
-** Return a pointer to the first non-whitespace character in z
148
-*/
149
-static const char *firstToken(const char *z){
150
- while( fossil_isspace(*z) ){
151
- z++;
152
- }
153
- return z;
154
-}
155
-
156
-/*
157
-** The n-bytes of content in z is a single multipart mime segment
158
-** with its own header and body. Decode this one segment and add it to p;
159
-**
160
-** Rows of the header of the segment are added to p if bAddHeader is
161
-** true.
162
-*/
163
-LOCAL void emailtoc_add_multipart_segment(
164
- EmailToc *p, /* Append the segments here */
165
- char *z, /* The body component */
166
- int bAddHeader /* True to add header lines to p */
167
-){
168
- int i, j;
169
- int n;
170
- int multipartBody = 0;
171
- EmailBody *pBody = emailtoc_new_body(p);
172
- i = 0;
173
- while( z[i] ){
174
- n = email_line_length(&z[i]);
175
- if( (n==2 && z[i]=='\r' && z[i+1]=='\n') || z[i]=='\n' || n==0 ){
176
- /* This is the blank line at the end of the header */
177
- i += n;
178
- break;
179
- }
180
- for(j=i+n; j>i && fossil_isspace(z[j-1]); j--){}
181
- z[j] = 0;
182
- if( sqlite3_strnicmp(z+i, "Content-Type:", 13)==0 ){
183
- const char *z2 = firstToken(z+i+13);
184
- if( z2 && strncmp(z2, "multipart/", 10)==0 ){
185
- multipartBody = 1;
186
- }else{
187
- int j;
188
- for(j=0; z2[j]=='/' || fossil_isalnum(z2[j]); j++){}
189
- if( j>=sizeof(pBody->zMimetype) ) j = sizeof(pBody->zMimetype);
190
- memcpy(pBody->zMimetype, z2, j);
191
- pBody->zMimetype[j] = 0;
192
- }
193
- }
194
- /* 123456789 123456789 123456 */
195
- if( sqlite3_strnicmp(z+i, "Content-Transfer-Encoding:", 26)==0 ){
196
- const char *z2 = firstToken(z+(i+26));
197
- if( z2 && sqlite3_strnicmp(z2, "base64", 6)==0 ){
198
- pBody->encoding = EMAILENC_B64;
199
- /* 123456789 123456 */
200
- }else if( sqlite3_strnicmp(z2, "quoted-printable", 16)==0 ){
201
- pBody->encoding = EMAILENC_QUOTED;
202
- }else{
203
- pBody->encoding = EMAILENC_NONE;
204
- }
205
- }
206
- if( bAddHeader ){
207
- emailtoc_new_header_line(p, z+i);
208
- }else if( sqlite3_strnicmp(z+i, "Content-Disposition:", 20)==0 ){
209
- /* 123456789 123456789 */
210
- fossil_free(pBody->zFilename);
211
- pBody->zFilename = email_hdr_value(z+i, "filename");
212
- }
213
- i += n;
214
- }
215
- if( multipartBody ){
216
- p->nBody--;
217
- emailtoc_add_multipart(p, z+i);
218
- }else{
219
- pBody->zContent = z+i;
220
- }
221
-}
222
-
223
-/*
224
-** The n-bytes of content in z are a multipart/ body component for
225
-** an email message. Decode this into its individual segments.
226
-**
227
-** The component should start and end with a boundary line. There
228
-** may be additional boundary lines in the middle.
229
-*/
230
-LOCAL void emailtoc_add_multipart(
231
- EmailToc *p, /* Append the segments here */
232
- char *z /* The body component. zero-terminated */
233
-){
234
- int nB; /* Size of the boundary string */
235
- int iStart; /* Start of the coding region past boundary mark */
236
- int i; /* Loop index */
237
- char *zBoundary = 0; /* Boundary marker */
238
-
239
- /* Skip forward to the beginning of the boundary mark. The boundary
240
- ** mark always begins with "--" */
241
- while( z[0]!='-' || z[1]!='-' ){
242
- while( z[0] && z[0]!='\n' ) z++;
243
- if( z[0]==0 ) return;
244
- z++;
245
- }
246
-
247
- /* Find the length of the boundary mark. */
248
- zBoundary = z;
249
- for(nB=0; z[nB] && !fossil_isspace(z[nB]); nB++){}
250
- if( nB==0 ) return;
251
-
252
- z += nB;
253
- while( fossil_isspace(z[0]) ) z++;
254
- zBoundary[nB] = 0;
255
- for(i=iStart=0; z[i]; i++){
256
- if( z[i]=='\n' && strncmp(z+i+1, zBoundary, nB)==0 ){
257
- z[i+1] = 0;
258
- emailtoc_add_multipart_segment(p, z+iStart, 0);
259
- iStart = i+nB;
260
- if( z[iStart]=='-' && z[iStart+1]=='-' ) return;
261
- while( fossil_isspace(z[iStart]) ) iStart++;
262
- i = iStart;
263
- }
264
- }
265
-}
266
-
267
-/*
268
-** Compute a table-of-contents (EmailToc) for the email message
269
-** provided on the input.
270
-**
271
-** This routine will cause pEmail to become zero-terminated if it is
272
-** not already. It will also insert zero characters into parts of
273
-** the message, to delimit the various components.
274
-*/
275
-EmailToc *emailtoc_from_email(Blob *pEmail){
276
- char *z;
277
- EmailToc *p = emailtoc_alloc();
278
- blob_terminate(pEmail);
279
- z = blob_buffer(pEmail);
280
- emailtoc_add_multipart_segment(p, z, 1);
281
- return p;
282
-}
283
-
284
-/*
285
-** Inplace-unfolding of an email header line.
286
-**
287
-** Actually - this routine works by converting all contiguous sequences
288
-** of whitespace into a single space character.
289
-*/
290
-static void email_hdr_unfold(char *z){
291
- int i, j;
292
- char c;
293
- for(i=j=0; (c = z[i])!=0; i++){
294
- if( fossil_isspace(c) ){
295
- c = ' ';
296
- if( j && z[j-1]==' ' ) continue;
297
- }
298
- z[j++] = c;
299
- }
300
- z[j] = 0;
301
-}
302
-
303
-/*
304
-** COMMAND: test-decode-email
305
-**
306
-** Usage: %fossil test-decode-email FILE
307
-**
308
-** Read an rfc-2822 formatted email out of FILE, then write a decoding
309
-** to stdout. Use for testing and validating the email decoder.
310
-*/
311
-void test_email_decode_cmd(void){
312
- Blob email;
313
- EmailToc *p;
314
- int i;
315
- verify_all_options();
316
- if( g.argc!=3 ) usage("FILE");
317
- blob_read_from_file(&email, g.argv[2], ExtFILE);
318
- p = emailtoc_from_email(&email);
319
- fossil_print("%d header line and %d content segments\n",
320
- p->nHdr, p->nBody);
321
- for(i=0; i<p->nHdr; i++){
322
- email_hdr_unfold(p->azHdr[i]);
323
- fossil_print("%3d: %s\n", i, p->azHdr[i]);
324
- }
325
- for(i=0; i<p->nBody; i++){
326
- fossil_print("\nBODY %d mime \"%s\" encoding %d",
327
- i, p->aBody[i].zMimetype, p->aBody[i].encoding);
328
- if( p->aBody[i].zFilename ){
329
- fossil_print(" filename \"%s\"", p->aBody[i].zFilename);
330
- }
331
- fossil_print("\n");
332
- if( strncmp(p->aBody[i].zMimetype,"text/",5)!=0 ) continue;
333
- switch( p->aBody[i].encoding ){
334
- case EMAILENC_B64: {
335
- int n = 0;
336
- decodeBase64(p->aBody[i].zContent, &n, p->aBody[i].zContent);
337
- fossil_print("%s", p->aBody[i].zContent);
338
- if( n && p->aBody[i].zContent[n-1]!='\n' ) fossil_print("\n");
339
- break;
340
- }
341
- case EMAILENC_QUOTED: {
342
- int n = 0;
343
- decodeQuotedPrintable(p->aBody[i].zContent, &n);
344
- fossil_print("%s", p->aBody[i].zContent);
345
- if( n && p->aBody[i].zContent[n-1]!='\n' ) fossil_print("\n");
346
- break;
347
- }
348
- default: {
349
- fossil_print("%s\n", p->aBody[i].zContent);
350
- break;
351
- }
352
- }
353
- }
354
- emailtoc_free(p);
355
- blob_reset(&email);
356
-}
357
-
358
-/*
359
-** Add the select/option box to the timeline submenu that shows
360
-** the various email message formats.
361
-*/
362
-static void webmail_f_submenu(void){
363
- static const char *const az[] = {
364
- "0", "Normal",
365
- "1", "Decoded",
366
- "2", "Raw",
367
- };
368
- style_submenu_multichoice("f", sizeof(az)/(2*sizeof(az[0])), az, 0);
369
-}
370
-
371
-/*
372
-** If the first N characters of z[] are the name of a header field
373
-** that should be shown in "Normal" mode, then return 1.
374
-*/
375
-static int webmail_normal_header(const char *z, int N){
376
- static const char *const az[] = {
377
- "To", "Cc", "Bcc", "Date", "From", "Subject",
378
- };
379
- int i;
380
- for(i=0; i<sizeof(az)/sizeof(az[0]); i++){
381
- if( sqlite3_strnicmp(z, az[i], N)==0 ) return 1;
382
- }
383
- return 0;
384
-}
385
-
386
-/*
387
-** Paint a page showing a single email message
388
-*/
389
-static void webmail_show_one_message(
390
- HQuery *pUrl, /* Calling context */
391
- int emailid, /* emailbox.ebid to display */
392
- const char *zUser /* User who owns it, or NULL if does not matter */
393
-){
394
- Blob sql;
395
- Stmt q;
396
- int eState = -1;
397
- int eTranscript = 0;
398
- char zENum[30];
399
- style_submenu_element("Index", "%s", url_render(pUrl,"id",0,0,0));
400
- webmail_f_submenu();
401
- blob_init(&sql, 0, 0);
402
- db_begin_transaction();
403
- blob_append_sql(&sql,
404
- "SELECT decompress(etxt), estate, emailblob.ets"
405
- " FROM emailblob, emailbox"
406
- " WHERE emailid=emsgid AND ebid=%d",
407
- emailid
408
- );
409
- if( zUser ) blob_append_sql(&sql, " AND euser=%Q", zUser);
410
- db_prepare_blob(&q, &sql);
411
- blob_reset(&sql);
412
- style_set_current_feature("webmail");
413
- style_header("Message %d",emailid);
414
- if( db_step(&q)==SQLITE_ROW ){
415
- Blob msg = db_column_text_as_blob(&q, 0);
416
- int eFormat = atoi(PD("f","0"));
417
- eState = db_column_int(&q, 1);
418
- eTranscript = db_column_int(&q, 2);
419
- if( eFormat==2 ){
420
- @ <pre>%h(db_column_text(&q, 0))</pre>
421
- }else{
422
- EmailToc *p = emailtoc_from_email(&msg);
423
- int i, j;
424
- @ <p>
425
- for(i=0; i<p->nHdr; i++){
426
- char *z = p->azHdr[i];
427
- email_hdr_unfold(z);
428
- for(j=0; z[j] && z[j]!=':'; j++){}
429
- if( eFormat==0 && !webmail_normal_header(z, j) ) continue;
430
- if( z[j]!=':' ){
431
- @ %h(z)<br>
432
- }else{
433
- z[j] = 0;
434
- @ <b>%h(z):</b> %h(z+j+1)<br>
435
- }
436
- }
437
- for(i=0; i<p->nBody; i++){
438
- @ <hr><b>Messsage Body #%d(i): %h(p->aBody[i].zMimetype) \
439
- if( p->aBody[i].zFilename ){
440
- @ "%h(p->aBody[i].zFilename)"
441
- }
442
- @ </b>
443
- if( eFormat==0 ){
444
- if( strncmp(p->aBody[i].zMimetype, "text/plain", 10)!=0 ) continue;
445
- if( p->aBody[i].zFilename ) continue;
446
- }else{
447
- if( strncmp(p->aBody[i].zMimetype, "text/", 5)!=0 ) continue;
448
- }
449
- switch( p->aBody[i].encoding ){
450
- case EMAILENC_B64: {
451
- int n = 0;
452
- decodeBase64(p->aBody[i].zContent, &n, p->aBody[i].zContent);
453
- break;
454
- }
455
- case EMAILENC_QUOTED: {
456
- int n = 0;
457
- decodeQuotedPrintable(p->aBody[i].zContent, &n);
458
- break;
459
- }
460
- }
461
- @ <pre>%h(p->aBody[i].zContent)</pre>
462
- }
463
- }
464
- }
465
- db_finalize(&q);
466
-
467
- /* Optionally show the SMTP transcript */
468
- if( eTranscript>0
469
- && db_exists("SELECT 1 FROM emailblob WHERE emailid=%d", eTranscript)
470
- ){
471
- if( P("ts")==0 ){
472
- sqlite3_snprintf(sizeof(zENum), zENum, "%d", emailid);
473
- style_submenu_element("SMTP Transcript","%s",
474
- url_render(pUrl, "ts", "1", "id", zENum));
475
- }else{
476
- db_prepare(&q,
477
- "SELECT decompress(etxt) FROM emailblob WHERE emailid=%d", eTranscript
478
- );
479
- if( db_step(&q)==SQLITE_ROW ){
480
- const char *zTranscript = db_column_text(&q, 0);
481
- @ <hr>
482
- @ <pre>%h(zTranscript)</pre>
483
- }
484
- db_finalize(&q);
485
- }
486
- }
487
-
488
- if( eState==0 ){
489
- /* If is message is currently Unread, change it to Read */
490
- blob_append_sql(&sql,
491
- "UPDATE emailbox SET estate=1 "
492
- " WHERE estate=0 AND ebid=%d",
493
- emailid
494
- );
495
- if( zUser ) blob_append_sql(&sql, " AND euser=%Q", zUser);
496
- db_multi_exec("%s", blob_sql_text(&sql));
497
- blob_reset(&sql);
498
- eState = 1;
499
- }
500
-
501
- url_add_parameter(pUrl, "id", 0);
502
- sqlite3_snprintf(sizeof(zENum), zENum, "e%d", emailid);
503
- if( eState==2 ){
504
- style_submenu_element("Undelete","%s",
505
- url_render(pUrl,"read","1",zENum,"1"));
506
- }
507
- if( eState==1 ){
508
- style_submenu_element("Delete", "%s",
509
- url_render(pUrl,"trash","1",zENum,"1"));
510
- style_submenu_element("Mark As Unread", "%s",
511
- url_render(pUrl,"unread","1",zENum,"1"));
512
- }
513
- if( eState==3 ){
514
- style_submenu_element("Delete", "%s",
515
- url_render(pUrl,"trash","1",zENum,"1"));
516
- }
517
-
518
- db_end_transaction(0);
519
- style_finish_page();
520
- return;
521
-}
522
-
523
-/*
524
-** Scan the query parameters looking for parameters with name of the
525
-** form "eN" where N is an integer. For all such integers, change
526
-** the state of every emailbox entry with ebid==N to eStateNew provided
527
-** that either zUser is NULL or matches.
528
-**
529
-** Or if eNewState==99, then delete the entries.
530
-*/
531
-static void webmail_change_state(int eNewState, const char *zUser){
532
- Blob sql;
533
- int sep = '(';
534
- int i;
535
- const char *zName;
536
- int n;
537
- if( !cgi_csrf_safe(0) ) return;
538
- blob_init(&sql, 0, 0);
539
- if( eNewState==99 ){
540
- blob_append_sql(&sql, "DELETE FROM emailbox WHERE estate==2 AND ebid IN ");
541
- }else{
542
- blob_append_sql(&sql, "UPDATE emailbox SET estate=%d WHERE ebid IN ",
543
- eNewState);
544
- }
545
- for(i=0; (zName = cgi_parameter_name(i))!=0; i++){
546
- if( zName[0]!='e' ) continue;
547
- if( !fossil_isdigit(zName[1]) ) continue;
548
- n = atoi(zName+1);
549
- blob_append_sql(&sql, "%c%d", sep, n);
550
- sep = ',';
551
- }
552
- if( zUser ){
553
- blob_append_sql(&sql, ") AND euser=%Q", zUser);
554
- }else{
555
- blob_append_sql(&sql, ")");
556
- }
557
- if( sep==',' ){
558
- db_multi_exec("%s", blob_sql_text(&sql));
559
- }
560
- blob_reset(&sql);
561
-}
562
-
563
-
564
-/*
565
-** Add the select/option box to the timeline submenu that shows
566
-** which messages to include in the index.
567
-*/
568
-static void webmail_d_submenu(void){
569
- static const char *const az[] = {
570
- "0", "InBox",
571
- "1", "Unread",
572
- "2", "Trash",
573
- "3", "Sent",
574
- "4", "Everything",
575
- };
576
- style_submenu_multichoice("d", sizeof(az)/(2*sizeof(az[0])), az, 0);
577
-}
578
-
579
-/*
580
-** WEBPAGE: webmail
581
-**
582
-** This page can be used to read content from the EMAILBOX table
583
-** that contains email received by the "fossil smtpd" command.
584
-**
585
-** Query parameters:
586
-**
587
-** id=N Show a single email entry emailbox.ebid==N
588
-** f=N Display format. 0: decoded 1: raw
589
-** user=USER Show mailbox for USER (admin only).
590
-** user=* Show mailbox for all users (admin only).
591
-** d=N 0: inbox+unread 1: unread-only 2: trash 3: all
592
-** eN Select email entry emailbox.ebid==N
593
-** trash Move selected entries to trash (estate=2)
594
-** read Mark selected entries as read (estate=1)
595
-** unread Mark selected entries as unread (estate=0)
596
-**
597
-*/
598
-void webmail_page(void){
599
- int emailid;
600
- Stmt q;
601
- Blob sql;
602
- int showAll = 0;
603
- const char *zUser = 0;
604
- int d = 0; /* Display mode. 0..3. d= query parameter */
605
- int pg = 0; /* Page number */
606
- int N = 50; /* Results per page */
607
- int got; /* Number of results on this page */
608
- char zPPg[30]; /* Previous page */
609
- char zNPg[30]; /* Next page */
610
- HQuery url;
611
- login_check_credentials();
612
- if( !login_is_individual() ){
613
- login_needed(0);
614
- return;
615
- }
616
- style_set_current_feature("webmail");
617
- if( !db_table_exists("repository","emailbox") ){
618
- style_header("Webmail Not Available");
619
- @ <p>This repository is not configured to provide webmail</p>
620
- style_finish_page();
621
- return;
622
- }
623
- add_content_sql_commands(g.db);
624
- emailid = atoi(PD("id","0"));
625
- url_initialize(&url, "webmail");
626
- if( g.perm.Admin ){
627
- zUser = PD("user",g.zLogin);
628
- if( zUser ){
629
- url_add_parameter(&url, "user", zUser);
630
- if( fossil_strcmp(zUser,"*")==0 ){
631
- showAll = 1;
632
- zUser = 0;
633
- }
634
- }
635
- }else{
636
- zUser = g.zLogin;
637
- }
638
- if( P("d") ) url_add_parameter(&url, "d", P("d"));
639
- if( emailid>0 ){
640
- webmail_show_one_message(&url, emailid, zUser);
641
- return;
642
- }
643
- style_header("Webmail");
644
- webmail_d_submenu();
645
- db_begin_transaction();
646
- if( P("trash")!=0 ) webmail_change_state(2,zUser);
647
- if( P("unread")!=0 ) webmail_change_state(0,zUser);
648
- if( P("read")!=0 ) webmail_change_state(1,zUser);
649
- if( P("purge")!=0 ) webmail_change_state(99,zUser);
650
- blob_init(&sql, 0, 0);
651
- blob_append_sql(&sql,
652
- "CREATE TEMP TABLE tmbox AS "
653
- "SELECT ebid," /* 0 */
654
- " efrom," /* 1 */
655
- " datetime(edate,'unixepoch')," /* 2 */
656
- " estate," /* 3 */
657
- " esubject," /* 4 */
658
- " euser" /* 5 */
659
- " FROM emailbox"
660
- );
661
- d = atoi(PD("d","0"));
662
- switch( d ){
663
- case 0: { /* Show unread and read */
664
- blob_append_sql(&sql, " WHERE estate<=1");
665
- break;
666
- }
667
- case 1: { /* Unread messages only */
668
- blob_append_sql(&sql, " WHERE estate=0");
669
- break;
670
- }
671
- case 2: { /* Trashcan only */
672
- blob_append_sql(&sql, " WHERE estate=2");
673
- break;
674
- }
675
- case 3: { /* Outgoing email only */
676
- blob_append_sql(&sql, " WHERE estate=3");
677
- break;
678
- }
679
- case 4: { /* Everything */
680
- blob_append_sql(&sql, " WHERE 1");
681
- break;
682
- }
683
- }
684
- if( showAll ){
685
- style_submenu_element("My Emails", "%s", url_render(&url,"user",0,0,0));
686
- }else if( zUser!=0 ){
687
- style_submenu_element("All Users", "%s", url_render(&url,"user","*",0,0));
688
- if( fossil_strcmp(zUser, g.zLogin)!=0 ){
689
- style_submenu_element("My Emails", "%s", url_render(&url,"user",0,0,0));
690
- }
691
- if( zUser ){
692
- blob_append_sql(&sql, " AND euser=%Q", zUser);
693
- }else{
694
- blob_append_sql(&sql, " AND euser=%Q", g.zLogin);
695
- }
696
- }else{
697
- if( g.perm.Admin ){
698
- style_submenu_element("All Users", "%s", url_render(&url,"user","*",0,0));
699
- }
700
- blob_append_sql(&sql, " AND euser=%Q", g.zLogin);
701
- }
702
- pg = atoi(PD("pg","0"));
703
- blob_append_sql(&sql, " ORDER BY edate DESC limit %d offset %d", N+1, pg*N);
704
- db_multi_exec("%s", blob_sql_text(&sql));
705
- got = db_int(0, "SELECT count(*) FROM tmbox");
706
- db_prepare(&q, "SELECT * FROM tmbox LIMIT %d", N);
707
- blob_reset(&sql);
708
- @ <form action="%R/webmail" method="POST">
709
- @ <input type="hidden" name="d" value="%d(d)">
710
- @ <input type="hidden" name="user" value="%h(zUser?zUser:"*")">
711
- @ <table border="0" width="100%%">
712
- @ <tr><td align="left">
713
- if( d==2 ){
714
- @ <input type="submit" name="read" value="Undelete">
715
- @ <input type="submit" name="purge" value="Delete Permanently">
716
- }else{
717
- @ <input type="submit" name="trash" value="Delete">
718
- if( d!=1 ){
719
- @ <input type="submit" name="unread" value="Mark as unread">
720
- }
721
- @ <input type="submit" name="read" value="Mark as read">
722
- }
723
- @ <button onclick="webmailSelectAll(); return false;">Select All</button>
724
- @ <a href="%h(url_render(&url,0,0,0,0))">refresh</a>
725
- @ </td><td align="right">
726
- if( pg>0 ){
727
- sqlite3_snprintf(sizeof(zPPg), zPPg, "%d", pg-1);
728
- @ <a href="%s(url_render(&url,"pg",zPPg,0,0))">&lt; Newer</a>&nbsp;&nbsp;
729
- }
730
- if( got>50 ){
731
- sqlite3_snprintf(sizeof(zNPg),zNPg,"%d",pg+1);
732
- @ <a href="%s(url_render(&url,"pg",zNPg,0,0))">Older &gt;</a></td>
733
- }
734
- @ </table>
735
- @ <table>
736
- while( db_step(&q)==SQLITE_ROW ){
737
- const char *zId = db_column_text(&q,0);
738
- const char *zFrom = db_column_text(&q, 1);
739
- const char *zDate = db_column_text(&q, 2);
740
- const char *zSubject = db_column_text(&q, 4);
741
- if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)";
742
- @ <tr>
743
- @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td>
744
- @ <td>%h(zFrom)</td>
745
- @ <td><a href="%h(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \
746
- @ %s(zDate)</td>
747
- if( showAll ){
748
- const char *zTo = db_column_text(&q,5);
749
- @ <td><a href="%h(url_render(&url,"user",zTo,0,0))">%h(zTo)</a></td>
750
- }
751
- @ </tr>
752
- }
753
- db_finalize(&q);
754
- @ </table>
755
- @ </form>
756
- @ <script>
757
- @ function webmailSelectAll(){
758
- @ var x = document.getElementsByClassName("webmailckbox");
759
- @ for(i=0; i<x.length; i++){
760
- @ x[i].checked = true;
761
- @ }
762
- @ }
763
- @ </script>
764
- style_finish_page();
765
- db_end_transaction(0);
766
-}
767
-
768
-/*
769
-** WEBPAGE: emailblob
770
-**
771
-** This page, accessible only to administrators, allows easy viewing of
772
-** the emailblob table - the table that contains the text of email messages
773
-** both inbound and outbound, and transcripts of SMTP sessions.
774
-**
775
-** id=N Show the text of emailblob with emailid==N
776
-**
777
-*/
778
-void webmail_emailblob_page(void){
779
- int id = atoi(PD("id","0"));
780
- Stmt q;
781
- login_check_credentials();
782
- if( !g.perm.Setup ){
783
- login_needed(0);
784
- return;
785
- }
786
- add_content_sql_commands(g.db);
787
- style_set_current_feature("webmail");
788
- style_header("emailblob table");
789
- if( id>0 ){
790
- style_submenu_element("Index", "%R/emailblob");
791
- @ <ul>
792
- db_prepare(&q, "SELECT emailid FROM emailblob WHERE ets=%d", id);
793
- while( db_step(&q)==SQLITE_ROW ){
794
- int id = db_column_int(&q, 0);
795
- @ <li> <a href="%R/emailblob?id=%d(id)">emailblob entry %d(id)</a>
796
- }
797
- db_finalize(&q);
798
- db_prepare(&q, "SELECT euser, estate FROM emailbox WHERE emsgid=%d", id);
799
- while( db_step(&q)==SQLITE_ROW ){
800
- const char *zUser = db_column_text(&q, 0);
801
- int e = db_column_int(&q, 1);
802
- @ <li> emailbox for %h(zUser) state %d(e)
803
- }
804
- db_finalize(&q);
805
- db_prepare(&q, "SELECT efrom, eto FROM emailoutq WHERE emsgid=%d", id);
806
- while( db_step(&q)==SQLITE_ROW ){
807
- const char *zFrom = db_column_text(&q, 0);
808
- const char *zTo = db_column_text(&q, 1);
809
- @ <li> emailoutq message body from %h(zFrom) to %h(zTo)
810
- }
811
- db_finalize(&q);
812
- db_prepare(&q, "SELECT efrom, eto FROM emailoutq WHERE ets=%d", id);
813
- while( db_step(&q)==SQLITE_ROW ){
814
- const char *zFrom = db_column_text(&q, 0);
815
- const char *zTo = db_column_text(&q, 1);
816
- @ <li> emailoutq transcript from %h(zFrom) to %h(zTo)
817
- }
818
- db_finalize(&q);
819
- @ </ul>
820
- @ <hr>
821
- db_prepare(&q, "SELECT decompress(etxt) FROM emailblob WHERE emailid=%d",
822
- id);
823
- while( db_step(&q)==SQLITE_ROW ){
824
- const char *zContent = db_column_text(&q, 0);
825
- @ <pre>%h(zContent)</pre>
826
- }
827
- db_finalize(&q);
828
- }else{
829
- style_submenu_element("emailoutq table","%R/emailoutq");
830
- db_prepare(&q,
831
- "SELECT emailid, enref, ets, datetime(etime,'unixepoch'), esz,"
832
- " length(etxt)"
833
- " FROM emailblob ORDER BY etime DESC, emailid DESC");
834
- @ <table border="1" cellpadding="5" cellspacing="0" class="sortable" \
835
- @ data-column-types='nnntkk'>
836
- @ <thead><tr><th> emailid <th> enref <th> ets <th> etime \
837
- @ <th> uncompressed <th> compressed </tr></thead><tbody>
838
- while( db_step(&q)==SQLITE_ROW ){
839
- int id = db_column_int(&q, 0);
840
- int nref = db_column_int(&q, 1);
841
- int ets = db_column_int(&q, 2);
842
- const char *zDate = db_column_text(&q, 3);
843
- int sz = db_column_int(&q,4);
844
- int csz = db_column_int(&q,5);
845
- @ <tr>
846
- @ <td align="right"><a href="%R/emailblob?id=%d(id)">%d(id)</a>
847
- @ <td align="right">%d(nref)</td>
848
- if( ets>0 ){
849
- @ <td align="right">%d(ets)</td>
850
- }else{
851
- @ <td>&nbsp;</td>
852
- }
853
- @ <td>%h(zDate)</td>
854
- @ <td align="right" data-sortkey='%08x(sz)'>%,d(sz)</td>
855
- @ <td align="right" data-sortkey='%08x(csz)'>%,d(csz)</td>
856
- @ </tr>
857
- }
858
- @ </tbody></table>
859
- db_finalize(&q);
860
- style_table_sorter();
861
- }
862
- style_finish_page();
863
-}
864
-
865
-/*
866
-** WEBPAGE: emailoutq
867
-**
868
-** This page, accessible only to administrators, allows easy viewing of
869
-** the emailoutq table - the table that contains the email messages
870
-** that are queued for transmission via SMTP.
871
-*/
872
-void webmail_emailoutq_page(void){
873
- Stmt q;
874
- login_check_credentials();
875
- if( !g.perm.Setup ){
876
- login_needed(0);
877
- return;
878
- }
879
- add_content_sql_commands(g.db);
880
- style_set_current_feature("webmail");
881
- style_header("emailoutq table");
882
- style_submenu_element("emailblob table","%R/emailblob");
883
- db_prepare(&q,
884
- "SELECT edomain, efrom, eto, emsgid, "
885
- " datetime(ectime,'unixepoch'),"
886
- " datetime(nullif(emtime,0),'unixepoch'),"
887
- " ensend, ets"
888
- " FROM emailoutq"
889
- );
890
- @ <table border="1" cellpadding="5" cellspacing="0" class="sortable" \
891
- @ data-column-types='tttnttnn'>
892
- @ <thead><tr><th> edomain <th> efrom <th> eto <th> emsgid \
893
- @ <th> ectime <th> emtime <th> ensend <th> ets </tr></thead><tbody>
894
- while( db_step(&q)==SQLITE_ROW ){
895
- const char *zDomain = db_column_text(&q, 0);
896
- const char *zFrom = db_column_text(&q, 1);
897
- const char *zTo = db_column_text(&q, 2);
898
- int emsgid = db_column_int(&q, 3);
899
- const char *zCTime = db_column_text(&q, 4);
900
- const char *zMTime = db_column_text(&q, 5);
901
- int ensend = db_column_int(&q, 6);
902
- int ets = db_column_int(&q, 7);
903
- @ <tr>
904
- @ <td>%h(zDomain)
905
- @ <td>%h(zFrom)
906
- @ <td>%h(zTo)
907
- @ <td align="right"><a href="%R/emailblob?id=%d(emsgid)">%d(emsgid)</a>
908
- @ <td>%h(zCTime)
909
- @ <td>%h(zMTime)
910
- @ <td align="right">%d(ensend)
911
- if( ets>0 ){
912
- @ <td align="right"><a href="%R/emailblob?id=%d(ets)">%d(ets)</a></td>
913
- }else{
914
- @ <td>&nbsp;</td>
915
- }
916
- }
917
- @ </tbody></table>
918
- db_finalize(&q);
919
- style_table_sorter();
920
- style_finish_page();
921
-}
--- a/src/webmail.c
+++ b/src/webmail.c
@@ -1,921 +0,0 @@
1 /*
2 ** Copyright (c) 2018 D. Richard Hipp
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 **
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 **
12 ** Author contact information:
13 ** [email protected]
14 ** http://www.hwaci.com/drh/
15 **
16 *******************************************************************************
17 **
18 ** Implementation of web pages for managing the email storage tables
19 ** (if they exist):
20 **
21 ** emailbox
22 ** emailblob
23 ** emailroute
24 */
25 #include "config.h"
26 #include "webmail.h"
27 #include <assert.h>
28
29
30 #if INTERFACE
31
32 /* Recognized content encodings */
33 #define EMAILENC_NONE 0 /* No encoding */
34 #define EMAILENC_B64 1 /* Base64 encoded */
35 #define EMAILENC_QUOTED 2 /* Quoted printable */
36
37 /* An instance of the following object records the location of important
38 ** attributes on a single element in a multipart email message body.
39 */
40 struct EmailBody {
41 char zMimetype[32]; /* Mimetype */
42 u8 encoding; /* Type of encoding */
43 char *zFilename; /* From content-disposition: */
44 char *zContent; /* Content. \0 terminator inserted */
45 };
46
47 /*
48 ** An instance of the following object describes the struture of
49 ** an rfc-2822 email message.
50 */
51 struct EmailToc {
52 int nHdr; /* Number of header lines */
53 int nHdrAlloc; /* Number of header lines allocated */
54 char **azHdr; /* Pointer to header line. \0 terminator inserted */
55 int nBody; /* Number of body segments */
56 int nBodyAlloc; /* Number of body segments allocated */
57 EmailBody *aBody; /* Location of body information */
58 };
59 #endif
60
61 /*
62 ** Free An EmailToc object
63 */
64 void emailtoc_free(EmailToc *p){
65 int i;
66 fossil_free(p->azHdr);
67 for(i=0; i<p->nBody; i++){
68 fossil_free(p->aBody[i].zFilename);
69 }
70 fossil_free(p->aBody);
71 fossil_free(p);
72 }
73
74 /*
75 ** Allocate a new EmailToc object
76 */
77 EmailToc *emailtoc_alloc(void){
78 EmailToc *p = fossil_malloc( sizeof(*p) );
79 memset(p, 0, sizeof(*p));
80 return p;
81 }
82
83 /*
84 ** Add a new body element to an EmailToc.
85 */
86 EmailBody *emailtoc_new_body(EmailToc *p){
87 EmailBody *pNew;
88 p->nBody++;
89 if( p->nBody>p->nBodyAlloc ){
90 p->nBodyAlloc = (p->nBodyAlloc+1)*2;
91 p->aBody = fossil_realloc(p->aBody, sizeof(p->aBody[0])*p->nBodyAlloc);
92 }
93 pNew = &p->aBody[p->nBody-1];
94 memset(pNew, 0, sizeof(*pNew));
95 return pNew;
96 }
97
98 /*
99 ** Add a new header line to the EmailToc.
100 */
101 void emailtoc_new_header_line(EmailToc *p, char *z){
102 p->nHdr++;
103 if( p->nHdr>p->nHdrAlloc ){
104 p->nHdrAlloc = (p->nHdrAlloc+1)*2;
105 p->azHdr = fossil_realloc(p->azHdr, sizeof(p->azHdr[0])*p->nHdrAlloc);
106 }
107 p->azHdr[p->nHdr-1] = z;
108 }
109
110 /*
111 ** Return the length of a line in an email header. Continuation lines
112 ** are included. Hence, this routine returns the number of bytes up to
113 ** and including the first \n character that is followed by something
114 ** other than whitespace.
115 */
116 static int email_line_length(const char *z){
117 int i;
118 for(i=0; z[i] && (z[i]!='\n' || z[i+1]==' ' || z[i+1]=='\t'); i++){}
119 if( z[i]=='\n' ) i++;
120 return i;
121 }
122
123 /*
124 ** Look for a parameter of the form NAME=VALUE in the given email
125 ** header line. Return a copy of VALUE in space obtained from
126 ** fossil_malloc(). Or return NULL if there is no such parameter.
127 */
128 static char *email_hdr_value(const char *z, const char *zName){
129 int nName = (int)strlen(zName);
130 int i;
131 const char *z2 = strstr(z, zName);
132 if( z2==0 ) return 0;
133 z2 += nName;
134 if( z2[0]!='=' ) return 0;
135 z2++;
136 if( z2[0]=='"' ){
137 z2++;
138 for(i=0; z2[i] && z2[i]!='"'; i++){}
139 if( z2[i]!='"' ) return 0;
140 }else{
141 for(i=0; z2[i] && !fossil_isspace(z2[i]); i++){}
142 }
143 return mprintf("%.*s", i, z2);
144 }
145
146 /*
147 ** Return a pointer to the first non-whitespace character in z
148 */
149 static const char *firstToken(const char *z){
150 while( fossil_isspace(*z) ){
151 z++;
152 }
153 return z;
154 }
155
156 /*
157 ** The n-bytes of content in z is a single multipart mime segment
158 ** with its own header and body. Decode this one segment and add it to p;
159 **
160 ** Rows of the header of the segment are added to p if bAddHeader is
161 ** true.
162 */
163 LOCAL void emailtoc_add_multipart_segment(
164 EmailToc *p, /* Append the segments here */
165 char *z, /* The body component */
166 int bAddHeader /* True to add header lines to p */
167 ){
168 int i, j;
169 int n;
170 int multipartBody = 0;
171 EmailBody *pBody = emailtoc_new_body(p);
172 i = 0;
173 while( z[i] ){
174 n = email_line_length(&z[i]);
175 if( (n==2 && z[i]=='\r' && z[i+1]=='\n') || z[i]=='\n' || n==0 ){
176 /* This is the blank line at the end of the header */
177 i += n;
178 break;
179 }
180 for(j=i+n; j>i && fossil_isspace(z[j-1]); j--){}
181 z[j] = 0;
182 if( sqlite3_strnicmp(z+i, "Content-Type:", 13)==0 ){
183 const char *z2 = firstToken(z+i+13);
184 if( z2 && strncmp(z2, "multipart/", 10)==0 ){
185 multipartBody = 1;
186 }else{
187 int j;
188 for(j=0; z2[j]=='/' || fossil_isalnum(z2[j]); j++){}
189 if( j>=sizeof(pBody->zMimetype) ) j = sizeof(pBody->zMimetype);
190 memcpy(pBody->zMimetype, z2, j);
191 pBody->zMimetype[j] = 0;
192 }
193 }
194 /* 123456789 123456789 123456 */
195 if( sqlite3_strnicmp(z+i, "Content-Transfer-Encoding:", 26)==0 ){
196 const char *z2 = firstToken(z+(i+26));
197 if( z2 && sqlite3_strnicmp(z2, "base64", 6)==0 ){
198 pBody->encoding = EMAILENC_B64;
199 /* 123456789 123456 */
200 }else if( sqlite3_strnicmp(z2, "quoted-printable", 16)==0 ){
201 pBody->encoding = EMAILENC_QUOTED;
202 }else{
203 pBody->encoding = EMAILENC_NONE;
204 }
205 }
206 if( bAddHeader ){
207 emailtoc_new_header_line(p, z+i);
208 }else if( sqlite3_strnicmp(z+i, "Content-Disposition:", 20)==0 ){
209 /* 123456789 123456789 */
210 fossil_free(pBody->zFilename);
211 pBody->zFilename = email_hdr_value(z+i, "filename");
212 }
213 i += n;
214 }
215 if( multipartBody ){
216 p->nBody--;
217 emailtoc_add_multipart(p, z+i);
218 }else{
219 pBody->zContent = z+i;
220 }
221 }
222
223 /*
224 ** The n-bytes of content in z are a multipart/ body component for
225 ** an email message. Decode this into its individual segments.
226 **
227 ** The component should start and end with a boundary line. There
228 ** may be additional boundary lines in the middle.
229 */
230 LOCAL void emailtoc_add_multipart(
231 EmailToc *p, /* Append the segments here */
232 char *z /* The body component. zero-terminated */
233 ){
234 int nB; /* Size of the boundary string */
235 int iStart; /* Start of the coding region past boundary mark */
236 int i; /* Loop index */
237 char *zBoundary = 0; /* Boundary marker */
238
239 /* Skip forward to the beginning of the boundary mark. The boundary
240 ** mark always begins with "--" */
241 while( z[0]!='-' || z[1]!='-' ){
242 while( z[0] && z[0]!='\n' ) z++;
243 if( z[0]==0 ) return;
244 z++;
245 }
246
247 /* Find the length of the boundary mark. */
248 zBoundary = z;
249 for(nB=0; z[nB] && !fossil_isspace(z[nB]); nB++){}
250 if( nB==0 ) return;
251
252 z += nB;
253 while( fossil_isspace(z[0]) ) z++;
254 zBoundary[nB] = 0;
255 for(i=iStart=0; z[i]; i++){
256 if( z[i]=='\n' && strncmp(z+i+1, zBoundary, nB)==0 ){
257 z[i+1] = 0;
258 emailtoc_add_multipart_segment(p, z+iStart, 0);
259 iStart = i+nB;
260 if( z[iStart]=='-' && z[iStart+1]=='-' ) return;
261 while( fossil_isspace(z[iStart]) ) iStart++;
262 i = iStart;
263 }
264 }
265 }
266
267 /*
268 ** Compute a table-of-contents (EmailToc) for the email message
269 ** provided on the input.
270 **
271 ** This routine will cause pEmail to become zero-terminated if it is
272 ** not already. It will also insert zero characters into parts of
273 ** the message, to delimit the various components.
274 */
275 EmailToc *emailtoc_from_email(Blob *pEmail){
276 char *z;
277 EmailToc *p = emailtoc_alloc();
278 blob_terminate(pEmail);
279 z = blob_buffer(pEmail);
280 emailtoc_add_multipart_segment(p, z, 1);
281 return p;
282 }
283
284 /*
285 ** Inplace-unfolding of an email header line.
286 **
287 ** Actually - this routine works by converting all contiguous sequences
288 ** of whitespace into a single space character.
289 */
290 static void email_hdr_unfold(char *z){
291 int i, j;
292 char c;
293 for(i=j=0; (c = z[i])!=0; i++){
294 if( fossil_isspace(c) ){
295 c = ' ';
296 if( j && z[j-1]==' ' ) continue;
297 }
298 z[j++] = c;
299 }
300 z[j] = 0;
301 }
302
303 /*
304 ** COMMAND: test-decode-email
305 **
306 ** Usage: %fossil test-decode-email FILE
307 **
308 ** Read an rfc-2822 formatted email out of FILE, then write a decoding
309 ** to stdout. Use for testing and validating the email decoder.
310 */
311 void test_email_decode_cmd(void){
312 Blob email;
313 EmailToc *p;
314 int i;
315 verify_all_options();
316 if( g.argc!=3 ) usage("FILE");
317 blob_read_from_file(&email, g.argv[2], ExtFILE);
318 p = emailtoc_from_email(&email);
319 fossil_print("%d header line and %d content segments\n",
320 p->nHdr, p->nBody);
321 for(i=0; i<p->nHdr; i++){
322 email_hdr_unfold(p->azHdr[i]);
323 fossil_print("%3d: %s\n", i, p->azHdr[i]);
324 }
325 for(i=0; i<p->nBody; i++){
326 fossil_print("\nBODY %d mime \"%s\" encoding %d",
327 i, p->aBody[i].zMimetype, p->aBody[i].encoding);
328 if( p->aBody[i].zFilename ){
329 fossil_print(" filename \"%s\"", p->aBody[i].zFilename);
330 }
331 fossil_print("\n");
332 if( strncmp(p->aBody[i].zMimetype,"text/",5)!=0 ) continue;
333 switch( p->aBody[i].encoding ){
334 case EMAILENC_B64: {
335 int n = 0;
336 decodeBase64(p->aBody[i].zContent, &n, p->aBody[i].zContent);
337 fossil_print("%s", p->aBody[i].zContent);
338 if( n && p->aBody[i].zContent[n-1]!='\n' ) fossil_print("\n");
339 break;
340 }
341 case EMAILENC_QUOTED: {
342 int n = 0;
343 decodeQuotedPrintable(p->aBody[i].zContent, &n);
344 fossil_print("%s", p->aBody[i].zContent);
345 if( n && p->aBody[i].zContent[n-1]!='\n' ) fossil_print("\n");
346 break;
347 }
348 default: {
349 fossil_print("%s\n", p->aBody[i].zContent);
350 break;
351 }
352 }
353 }
354 emailtoc_free(p);
355 blob_reset(&email);
356 }
357
358 /*
359 ** Add the select/option box to the timeline submenu that shows
360 ** the various email message formats.
361 */
362 static void webmail_f_submenu(void){
363 static const char *const az[] = {
364 "0", "Normal",
365 "1", "Decoded",
366 "2", "Raw",
367 };
368 style_submenu_multichoice("f", sizeof(az)/(2*sizeof(az[0])), az, 0);
369 }
370
371 /*
372 ** If the first N characters of z[] are the name of a header field
373 ** that should be shown in "Normal" mode, then return 1.
374 */
375 static int webmail_normal_header(const char *z, int N){
376 static const char *const az[] = {
377 "To", "Cc", "Bcc", "Date", "From", "Subject",
378 };
379 int i;
380 for(i=0; i<sizeof(az)/sizeof(az[0]); i++){
381 if( sqlite3_strnicmp(z, az[i], N)==0 ) return 1;
382 }
383 return 0;
384 }
385
386 /*
387 ** Paint a page showing a single email message
388 */
389 static void webmail_show_one_message(
390 HQuery *pUrl, /* Calling context */
391 int emailid, /* emailbox.ebid to display */
392 const char *zUser /* User who owns it, or NULL if does not matter */
393 ){
394 Blob sql;
395 Stmt q;
396 int eState = -1;
397 int eTranscript = 0;
398 char zENum[30];
399 style_submenu_element("Index", "%s", url_render(pUrl,"id",0,0,0));
400 webmail_f_submenu();
401 blob_init(&sql, 0, 0);
402 db_begin_transaction();
403 blob_append_sql(&sql,
404 "SELECT decompress(etxt), estate, emailblob.ets"
405 " FROM emailblob, emailbox"
406 " WHERE emailid=emsgid AND ebid=%d",
407 emailid
408 );
409 if( zUser ) blob_append_sql(&sql, " AND euser=%Q", zUser);
410 db_prepare_blob(&q, &sql);
411 blob_reset(&sql);
412 style_set_current_feature("webmail");
413 style_header("Message %d",emailid);
414 if( db_step(&q)==SQLITE_ROW ){
415 Blob msg = db_column_text_as_blob(&q, 0);
416 int eFormat = atoi(PD("f","0"));
417 eState = db_column_int(&q, 1);
418 eTranscript = db_column_int(&q, 2);
419 if( eFormat==2 ){
420 @ <pre>%h(db_column_text(&q, 0))</pre>
421 }else{
422 EmailToc *p = emailtoc_from_email(&msg);
423 int i, j;
424 @ <p>
425 for(i=0; i<p->nHdr; i++){
426 char *z = p->azHdr[i];
427 email_hdr_unfold(z);
428 for(j=0; z[j] && z[j]!=':'; j++){}
429 if( eFormat==0 && !webmail_normal_header(z, j) ) continue;
430 if( z[j]!=':' ){
431 @ %h(z)<br>
432 }else{
433 z[j] = 0;
434 @ <b>%h(z):</b> %h(z+j+1)<br>
435 }
436 }
437 for(i=0; i<p->nBody; i++){
438 @ <hr><b>Messsage Body #%d(i): %h(p->aBody[i].zMimetype) \
439 if( p->aBody[i].zFilename ){
440 @ "%h(p->aBody[i].zFilename)"
441 }
442 @ </b>
443 if( eFormat==0 ){
444 if( strncmp(p->aBody[i].zMimetype, "text/plain", 10)!=0 ) continue;
445 if( p->aBody[i].zFilename ) continue;
446 }else{
447 if( strncmp(p->aBody[i].zMimetype, "text/", 5)!=0 ) continue;
448 }
449 switch( p->aBody[i].encoding ){
450 case EMAILENC_B64: {
451 int n = 0;
452 decodeBase64(p->aBody[i].zContent, &n, p->aBody[i].zContent);
453 break;
454 }
455 case EMAILENC_QUOTED: {
456 int n = 0;
457 decodeQuotedPrintable(p->aBody[i].zContent, &n);
458 break;
459 }
460 }
461 @ <pre>%h(p->aBody[i].zContent)</pre>
462 }
463 }
464 }
465 db_finalize(&q);
466
467 /* Optionally show the SMTP transcript */
468 if( eTranscript>0
469 && db_exists("SELECT 1 FROM emailblob WHERE emailid=%d", eTranscript)
470 ){
471 if( P("ts")==0 ){
472 sqlite3_snprintf(sizeof(zENum), zENum, "%d", emailid);
473 style_submenu_element("SMTP Transcript","%s",
474 url_render(pUrl, "ts", "1", "id", zENum));
475 }else{
476 db_prepare(&q,
477 "SELECT decompress(etxt) FROM emailblob WHERE emailid=%d", eTranscript
478 );
479 if( db_step(&q)==SQLITE_ROW ){
480 const char *zTranscript = db_column_text(&q, 0);
481 @ <hr>
482 @ <pre>%h(zTranscript)</pre>
483 }
484 db_finalize(&q);
485 }
486 }
487
488 if( eState==0 ){
489 /* If is message is currently Unread, change it to Read */
490 blob_append_sql(&sql,
491 "UPDATE emailbox SET estate=1 "
492 " WHERE estate=0 AND ebid=%d",
493 emailid
494 );
495 if( zUser ) blob_append_sql(&sql, " AND euser=%Q", zUser);
496 db_multi_exec("%s", blob_sql_text(&sql));
497 blob_reset(&sql);
498 eState = 1;
499 }
500
501 url_add_parameter(pUrl, "id", 0);
502 sqlite3_snprintf(sizeof(zENum), zENum, "e%d", emailid);
503 if( eState==2 ){
504 style_submenu_element("Undelete","%s",
505 url_render(pUrl,"read","1",zENum,"1"));
506 }
507 if( eState==1 ){
508 style_submenu_element("Delete", "%s",
509 url_render(pUrl,"trash","1",zENum,"1"));
510 style_submenu_element("Mark As Unread", "%s",
511 url_render(pUrl,"unread","1",zENum,"1"));
512 }
513 if( eState==3 ){
514 style_submenu_element("Delete", "%s",
515 url_render(pUrl,"trash","1",zENum,"1"));
516 }
517
518 db_end_transaction(0);
519 style_finish_page();
520 return;
521 }
522
523 /*
524 ** Scan the query parameters looking for parameters with name of the
525 ** form "eN" where N is an integer. For all such integers, change
526 ** the state of every emailbox entry with ebid==N to eStateNew provided
527 ** that either zUser is NULL or matches.
528 **
529 ** Or if eNewState==99, then delete the entries.
530 */
531 static void webmail_change_state(int eNewState, const char *zUser){
532 Blob sql;
533 int sep = '(';
534 int i;
535 const char *zName;
536 int n;
537 if( !cgi_csrf_safe(0) ) return;
538 blob_init(&sql, 0, 0);
539 if( eNewState==99 ){
540 blob_append_sql(&sql, "DELETE FROM emailbox WHERE estate==2 AND ebid IN ");
541 }else{
542 blob_append_sql(&sql, "UPDATE emailbox SET estate=%d WHERE ebid IN ",
543 eNewState);
544 }
545 for(i=0; (zName = cgi_parameter_name(i))!=0; i++){
546 if( zName[0]!='e' ) continue;
547 if( !fossil_isdigit(zName[1]) ) continue;
548 n = atoi(zName+1);
549 blob_append_sql(&sql, "%c%d", sep, n);
550 sep = ',';
551 }
552 if( zUser ){
553 blob_append_sql(&sql, ") AND euser=%Q", zUser);
554 }else{
555 blob_append_sql(&sql, ")");
556 }
557 if( sep==',' ){
558 db_multi_exec("%s", blob_sql_text(&sql));
559 }
560 blob_reset(&sql);
561 }
562
563
564 /*
565 ** Add the select/option box to the timeline submenu that shows
566 ** which messages to include in the index.
567 */
568 static void webmail_d_submenu(void){
569 static const char *const az[] = {
570 "0", "InBox",
571 "1", "Unread",
572 "2", "Trash",
573 "3", "Sent",
574 "4", "Everything",
575 };
576 style_submenu_multichoice("d", sizeof(az)/(2*sizeof(az[0])), az, 0);
577 }
578
579 /*
580 ** WEBPAGE: webmail
581 **
582 ** This page can be used to read content from the EMAILBOX table
583 ** that contains email received by the "fossil smtpd" command.
584 **
585 ** Query parameters:
586 **
587 ** id=N Show a single email entry emailbox.ebid==N
588 ** f=N Display format. 0: decoded 1: raw
589 ** user=USER Show mailbox for USER (admin only).
590 ** user=* Show mailbox for all users (admin only).
591 ** d=N 0: inbox+unread 1: unread-only 2: trash 3: all
592 ** eN Select email entry emailbox.ebid==N
593 ** trash Move selected entries to trash (estate=2)
594 ** read Mark selected entries as read (estate=1)
595 ** unread Mark selected entries as unread (estate=0)
596 **
597 */
598 void webmail_page(void){
599 int emailid;
600 Stmt q;
601 Blob sql;
602 int showAll = 0;
603 const char *zUser = 0;
604 int d = 0; /* Display mode. 0..3. d= query parameter */
605 int pg = 0; /* Page number */
606 int N = 50; /* Results per page */
607 int got; /* Number of results on this page */
608 char zPPg[30]; /* Previous page */
609 char zNPg[30]; /* Next page */
610 HQuery url;
611 login_check_credentials();
612 if( !login_is_individual() ){
613 login_needed(0);
614 return;
615 }
616 style_set_current_feature("webmail");
617 if( !db_table_exists("repository","emailbox") ){
618 style_header("Webmail Not Available");
619 @ <p>This repository is not configured to provide webmail</p>
620 style_finish_page();
621 return;
622 }
623 add_content_sql_commands(g.db);
624 emailid = atoi(PD("id","0"));
625 url_initialize(&url, "webmail");
626 if( g.perm.Admin ){
627 zUser = PD("user",g.zLogin);
628 if( zUser ){
629 url_add_parameter(&url, "user", zUser);
630 if( fossil_strcmp(zUser,"*")==0 ){
631 showAll = 1;
632 zUser = 0;
633 }
634 }
635 }else{
636 zUser = g.zLogin;
637 }
638 if( P("d") ) url_add_parameter(&url, "d", P("d"));
639 if( emailid>0 ){
640 webmail_show_one_message(&url, emailid, zUser);
641 return;
642 }
643 style_header("Webmail");
644 webmail_d_submenu();
645 db_begin_transaction();
646 if( P("trash")!=0 ) webmail_change_state(2,zUser);
647 if( P("unread")!=0 ) webmail_change_state(0,zUser);
648 if( P("read")!=0 ) webmail_change_state(1,zUser);
649 if( P("purge")!=0 ) webmail_change_state(99,zUser);
650 blob_init(&sql, 0, 0);
651 blob_append_sql(&sql,
652 "CREATE TEMP TABLE tmbox AS "
653 "SELECT ebid," /* 0 */
654 " efrom," /* 1 */
655 " datetime(edate,'unixepoch')," /* 2 */
656 " estate," /* 3 */
657 " esubject," /* 4 */
658 " euser" /* 5 */
659 " FROM emailbox"
660 );
661 d = atoi(PD("d","0"));
662 switch( d ){
663 case 0: { /* Show unread and read */
664 blob_append_sql(&sql, " WHERE estate<=1");
665 break;
666 }
667 case 1: { /* Unread messages only */
668 blob_append_sql(&sql, " WHERE estate=0");
669 break;
670 }
671 case 2: { /* Trashcan only */
672 blob_append_sql(&sql, " WHERE estate=2");
673 break;
674 }
675 case 3: { /* Outgoing email only */
676 blob_append_sql(&sql, " WHERE estate=3");
677 break;
678 }
679 case 4: { /* Everything */
680 blob_append_sql(&sql, " WHERE 1");
681 break;
682 }
683 }
684 if( showAll ){
685 style_submenu_element("My Emails", "%s", url_render(&url,"user",0,0,0));
686 }else if( zUser!=0 ){
687 style_submenu_element("All Users", "%s", url_render(&url,"user","*",0,0));
688 if( fossil_strcmp(zUser, g.zLogin)!=0 ){
689 style_submenu_element("My Emails", "%s", url_render(&url,"user",0,0,0));
690 }
691 if( zUser ){
692 blob_append_sql(&sql, " AND euser=%Q", zUser);
693 }else{
694 blob_append_sql(&sql, " AND euser=%Q", g.zLogin);
695 }
696 }else{
697 if( g.perm.Admin ){
698 style_submenu_element("All Users", "%s", url_render(&url,"user","*",0,0));
699 }
700 blob_append_sql(&sql, " AND euser=%Q", g.zLogin);
701 }
702 pg = atoi(PD("pg","0"));
703 blob_append_sql(&sql, " ORDER BY edate DESC limit %d offset %d", N+1, pg*N);
704 db_multi_exec("%s", blob_sql_text(&sql));
705 got = db_int(0, "SELECT count(*) FROM tmbox");
706 db_prepare(&q, "SELECT * FROM tmbox LIMIT %d", N);
707 blob_reset(&sql);
708 @ <form action="%R/webmail" method="POST">
709 @ <input type="hidden" name="d" value="%d(d)">
710 @ <input type="hidden" name="user" value="%h(zUser?zUser:"*")">
711 @ <table border="0" width="100%%">
712 @ <tr><td align="left">
713 if( d==2 ){
714 @ <input type="submit" name="read" value="Undelete">
715 @ <input type="submit" name="purge" value="Delete Permanently">
716 }else{
717 @ <input type="submit" name="trash" value="Delete">
718 if( d!=1 ){
719 @ <input type="submit" name="unread" value="Mark as unread">
720 }
721 @ <input type="submit" name="read" value="Mark as read">
722 }
723 @ <button onclick="webmailSelectAll(); return false;">Select All</button>
724 @ <a href="%h(url_render(&url,0,0,0,0))">refresh</a>
725 @ </td><td align="right">
726 if( pg>0 ){
727 sqlite3_snprintf(sizeof(zPPg), zPPg, "%d", pg-1);
728 @ <a href="%s(url_render(&url,"pg",zPPg,0,0))">&lt; Newer</a>&nbsp;&nbsp;
729 }
730 if( got>50 ){
731 sqlite3_snprintf(sizeof(zNPg),zNPg,"%d",pg+1);
732 @ <a href="%s(url_render(&url,"pg",zNPg,0,0))">Older &gt;</a></td>
733 }
734 @ </table>
735 @ <table>
736 while( db_step(&q)==SQLITE_ROW ){
737 const char *zId = db_column_text(&q,0);
738 const char *zFrom = db_column_text(&q, 1);
739 const char *zDate = db_column_text(&q, 2);
740 const char *zSubject = db_column_text(&q, 4);
741 if( zSubject==0 || zSubject[0]==0 ) zSubject = "(no subject)";
742 @ <tr>
743 @ <td><input type="checkbox" class="webmailckbox" name="e%s(zId)"></td>
744 @ <td>%h(zFrom)</td>
745 @ <td><a href="%h(url_render(&url,"id",zId,0,0))">%h(zSubject)</a> \
746 @ %s(zDate)</td>
747 if( showAll ){
748 const char *zTo = db_column_text(&q,5);
749 @ <td><a href="%h(url_render(&url,"user",zTo,0,0))">%h(zTo)</a></td>
750 }
751 @ </tr>
752 }
753 db_finalize(&q);
754 @ </table>
755 @ </form>
756 @ <script>
757 @ function webmailSelectAll(){
758 @ var x = document.getElementsByClassName("webmailckbox");
759 @ for(i=0; i<x.length; i++){
760 @ x[i].checked = true;
761 @ }
762 @ }
763 @ </script>
764 style_finish_page();
765 db_end_transaction(0);
766 }
767
768 /*
769 ** WEBPAGE: emailblob
770 **
771 ** This page, accessible only to administrators, allows easy viewing of
772 ** the emailblob table - the table that contains the text of email messages
773 ** both inbound and outbound, and transcripts of SMTP sessions.
774 **
775 ** id=N Show the text of emailblob with emailid==N
776 **
777 */
778 void webmail_emailblob_page(void){
779 int id = atoi(PD("id","0"));
780 Stmt q;
781 login_check_credentials();
782 if( !g.perm.Setup ){
783 login_needed(0);
784 return;
785 }
786 add_content_sql_commands(g.db);
787 style_set_current_feature("webmail");
788 style_header("emailblob table");
789 if( id>0 ){
790 style_submenu_element("Index", "%R/emailblob");
791 @ <ul>
792 db_prepare(&q, "SELECT emailid FROM emailblob WHERE ets=%d", id);
793 while( db_step(&q)==SQLITE_ROW ){
794 int id = db_column_int(&q, 0);
795 @ <li> <a href="%R/emailblob?id=%d(id)">emailblob entry %d(id)</a>
796 }
797 db_finalize(&q);
798 db_prepare(&q, "SELECT euser, estate FROM emailbox WHERE emsgid=%d", id);
799 while( db_step(&q)==SQLITE_ROW ){
800 const char *zUser = db_column_text(&q, 0);
801 int e = db_column_int(&q, 1);
802 @ <li> emailbox for %h(zUser) state %d(e)
803 }
804 db_finalize(&q);
805 db_prepare(&q, "SELECT efrom, eto FROM emailoutq WHERE emsgid=%d", id);
806 while( db_step(&q)==SQLITE_ROW ){
807 const char *zFrom = db_column_text(&q, 0);
808 const char *zTo = db_column_text(&q, 1);
809 @ <li> emailoutq message body from %h(zFrom) to %h(zTo)
810 }
811 db_finalize(&q);
812 db_prepare(&q, "SELECT efrom, eto FROM emailoutq WHERE ets=%d", id);
813 while( db_step(&q)==SQLITE_ROW ){
814 const char *zFrom = db_column_text(&q, 0);
815 const char *zTo = db_column_text(&q, 1);
816 @ <li> emailoutq transcript from %h(zFrom) to %h(zTo)
817 }
818 db_finalize(&q);
819 @ </ul>
820 @ <hr>
821 db_prepare(&q, "SELECT decompress(etxt) FROM emailblob WHERE emailid=%d",
822 id);
823 while( db_step(&q)==SQLITE_ROW ){
824 const char *zContent = db_column_text(&q, 0);
825 @ <pre>%h(zContent)</pre>
826 }
827 db_finalize(&q);
828 }else{
829 style_submenu_element("emailoutq table","%R/emailoutq");
830 db_prepare(&q,
831 "SELECT emailid, enref, ets, datetime(etime,'unixepoch'), esz,"
832 " length(etxt)"
833 " FROM emailblob ORDER BY etime DESC, emailid DESC");
834 @ <table border="1" cellpadding="5" cellspacing="0" class="sortable" \
835 @ data-column-types='nnntkk'>
836 @ <thead><tr><th> emailid <th> enref <th> ets <th> etime \
837 @ <th> uncompressed <th> compressed </tr></thead><tbody>
838 while( db_step(&q)==SQLITE_ROW ){
839 int id = db_column_int(&q, 0);
840 int nref = db_column_int(&q, 1);
841 int ets = db_column_int(&q, 2);
842 const char *zDate = db_column_text(&q, 3);
843 int sz = db_column_int(&q,4);
844 int csz = db_column_int(&q,5);
845 @ <tr>
846 @ <td align="right"><a href="%R/emailblob?id=%d(id)">%d(id)</a>
847 @ <td align="right">%d(nref)</td>
848 if( ets>0 ){
849 @ <td align="right">%d(ets)</td>
850 }else{
851 @ <td>&nbsp;</td>
852 }
853 @ <td>%h(zDate)</td>
854 @ <td align="right" data-sortkey='%08x(sz)'>%,d(sz)</td>
855 @ <td align="right" data-sortkey='%08x(csz)'>%,d(csz)</td>
856 @ </tr>
857 }
858 @ </tbody></table>
859 db_finalize(&q);
860 style_table_sorter();
861 }
862 style_finish_page();
863 }
864
865 /*
866 ** WEBPAGE: emailoutq
867 **
868 ** This page, accessible only to administrators, allows easy viewing of
869 ** the emailoutq table - the table that contains the email messages
870 ** that are queued for transmission via SMTP.
871 */
872 void webmail_emailoutq_page(void){
873 Stmt q;
874 login_check_credentials();
875 if( !g.perm.Setup ){
876 login_needed(0);
877 return;
878 }
879 add_content_sql_commands(g.db);
880 style_set_current_feature("webmail");
881 style_header("emailoutq table");
882 style_submenu_element("emailblob table","%R/emailblob");
883 db_prepare(&q,
884 "SELECT edomain, efrom, eto, emsgid, "
885 " datetime(ectime,'unixepoch'),"
886 " datetime(nullif(emtime,0),'unixepoch'),"
887 " ensend, ets"
888 " FROM emailoutq"
889 );
890 @ <table border="1" cellpadding="5" cellspacing="0" class="sortable" \
891 @ data-column-types='tttnttnn'>
892 @ <thead><tr><th> edomain <th> efrom <th> eto <th> emsgid \
893 @ <th> ectime <th> emtime <th> ensend <th> ets </tr></thead><tbody>
894 while( db_step(&q)==SQLITE_ROW ){
895 const char *zDomain = db_column_text(&q, 0);
896 const char *zFrom = db_column_text(&q, 1);
897 const char *zTo = db_column_text(&q, 2);
898 int emsgid = db_column_int(&q, 3);
899 const char *zCTime = db_column_text(&q, 4);
900 const char *zMTime = db_column_text(&q, 5);
901 int ensend = db_column_int(&q, 6);
902 int ets = db_column_int(&q, 7);
903 @ <tr>
904 @ <td>%h(zDomain)
905 @ <td>%h(zFrom)
906 @ <td>%h(zTo)
907 @ <td align="right"><a href="%R/emailblob?id=%d(emsgid)">%d(emsgid)</a>
908 @ <td>%h(zCTime)
909 @ <td>%h(zMTime)
910 @ <td align="right">%d(ensend)
911 if( ets>0 ){
912 @ <td align="right"><a href="%R/emailblob?id=%d(ets)">%d(ets)</a></td>
913 }else{
914 @ <td>&nbsp;</td>
915 }
916 }
917 @ </tbody></table>
918 db_finalize(&q);
919 style_table_sorter();
920 style_finish_page();
921 }
--- a/src/webmail.c
+++ b/src/webmail.c
@@ -1,921 +0,0 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Keyboard Shortcuts

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