wmail: more version and copyright updates.
[dockapps.git] / wmail / src / wmail.c
blob6178651a8ddf38c3607b633bb28ec0d7364f86d7
1 ///////////////////////////////////////////////////////////////////////////////
2 // wmail.c
3 // email indicator tool designed as docklet for Window Maker
4 // main c source-file
5 //
6 // Copyright 2000-2002, Sven Geisenhainer <sveng@informatik.uni-jena.de>.
7 // Copyright 2016-2017, Doug Torrance <dtorrance@piedmont.edu>.
8 // Copyright 2019, Jeremy Sowden <jeremy@azazel.net>.
9 //
10 // Redistribution and use in source and binary forms, with or without
11 // modification, are permitted provided that the following conditions
12 // are met:
13 // 1. Redistributions of source code must retain the above copyright
14 // notice, this list of conditions, and the following disclaimer.
15 // 2. Redistributions in binary form must reproduce the above copyright
16 // notice, this list of conditions, and the following disclaimer in the
17 // documentation and/or other materials provided with the distribution.
18 // 3. The name of the author may not be used to endorse or promote products
19 // derived from this software without specific prior written permission.
21 // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
22 // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
23 // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
24 // IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
25 // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26 // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30 // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 ///////////////////////////////////////////////////////////////////////////////
34 // includes
36 #ifdef HAVE_CONFIG_H
37 #ifndef CONFIG_H_INCLUDED
38 #include "../config.h"
39 #define CONFIG_H_INCLUDED
40 #endif
41 #endif
43 #include <ctype.h>
44 #include <stdio.h>
45 #include <stdlib.h>
46 #include <stdarg.h>
47 #include <string.h>
48 #ifdef HAVE_STRINGS_H
49 #include <strings.h>
50 #endif
51 #include <signal.h>
52 #include <utime.h>
53 #include <fnmatch.h>
54 #include <sys/time.h>
55 #include <sys/stat.h>
56 #include <dirent.h>
57 #include <X11/Xlib.h>
58 #ifdef HAVE_LIBDOCKAPP_DOCKAPP_H
59 #include <libdockapp/dockapp.h>
60 #else
61 #include <dockapp.h>
62 #endif
64 #include "common.h"
65 #include "config.h"
67 // pixmaps
68 #ifdef USE_DELT_XPMS
69 #include "xpm_delt/main.xpm"
70 #include "xpm_delt/symbols.xpm"
71 #include "xpm_delt/numbers.xpm"
72 #include "xpm_delt/button.xpm"
73 #include "xpm_delt/chars.xpm"
74 #else
75 #include "xpm/main.xpm"
76 #include "xpm/symbols.xpm"
77 #include "xpm/numbers.xpm"
78 #include "xpm/button.xpm"
79 #include "xpm/chars.xpm"
80 #endif
83 ///////////////////////////////////////////////////////////////////////////////
84 // typedefs
86 typedef enum {
87 FLAG_INITIAL = 0,
88 FLAG_READ = 1
89 } flag_t;
91 typedef struct _name_t {
92 char *name;
93 unsigned long checksum;
94 flag_t flag;
95 bool visited;
96 struct _name_t *next;
97 } name_t;
99 typedef enum {
100 STATE_NOMAIL,
101 STATE_NEWMAIL,
102 STATE_READMAIL
103 } mail_state_t;
105 typedef enum {
106 STATE_ADDRESS,
107 STATE_QUOTED_ADDRESS,
108 STATE_FULLNAME,
109 STATE_QUOTED_FULLNAME,
110 STATE_ENCODED_FULLNAME,
111 STATE_COMMENT
112 } parse_state_t;
115 ///////////////////////////////////////////////////////////////////////////////
116 // data
118 static unsigned long lastTimeOut;
119 static sig_atomic_t caughtSig;
120 static mail_state_t state;
121 static unsigned numMails;
122 static bool namesChanged;
123 static bool buttonPressed;
124 static bool readConfigFile;
125 static bool isMaildir;
126 static bool forceRead;
127 static bool forceRedraw = true;
128 static time_t lastModifySeconds;
129 static time_t lastAccessSeconds;
130 static Pixmap mainPixmap;
131 static Pixmap mainPixmap_mask;
132 static Pixmap symbolsPixmap;
133 static Pixmap charsPixmap;
134 static Pixmap numbersPixmap;
135 static Pixmap buttonPixmap;
136 static Pixmap outPixmap;
137 static GC tickerGC;
138 static XFontStruct *tickerFS;
139 static name_t *names;
140 static name_t *curTickerName;
142 static DAProgramOption options[] = {
143 {"-display", NULL, "display to use", DOString, False, {&config.display}},
144 {"-c", "--command", "cmd to run on btn-click (\"xterm -e mail\" is default)",
145 DOString, False, {&config.runCmd} },
146 {"-i", "--intervall",
147 "number of secs between mail-status updates (1 is default)", DONatural,
148 False, {&config.checkInterval} },
149 {"-f", "--familyname", "tickers the family-name if available", DONone,
150 False, {NULL} },
151 {"-fps", "--frames", "ticker frames per second", DONatural,
152 False, {&config.fps} },
153 {"-s", "--shortname", "tickers the nickname (all before the '@')", DONone,
154 False, {NULL} },
155 {"-sc", "--symbolcolor", "symbol color-name",
156 DOString, False, {&config.symbolColor} },
157 {"-fc", "--fontcolor", "ticker-font color-name",
158 DOString, False, {&config.fontColor} },
159 {"-bc", "--backcolor", "backlight color-name",
160 DOString, False, {&config.backColor} },
161 {"-oc", "--offcolor", "off-light color-name",
162 DOString, False, {&config.offlightColor} },
163 {"-bg", "--background", "frame-background for non-shaped window",
164 DOString, False, {&config.backgroundColor} },
165 {"-ns", "--noshape", "make the dockapp non-shaped (combine with -w)",
166 DONone, False, {NULL} },
167 {"-n", "--new", "forces wmail to show new mail exclusively", DONone, False, {NULL} },
168 {"-mb", "--mailbox", "specify another mailbox ($MAIL is default)", DOString, False, {&config.mailBox} },
169 {"-e", "--execute", "command to execute when receiving a new mail", DOString, False, {&config.cmdOnMail} },
170 {"-sf", "--statusfield", "consider the status-field of the mail header to distinguish unread mails", DONone, False, {NULL} },
171 {"-rs", "--readstatus", "status field content that your client uses to mark read mails", DOString, False, {&config.readStatus} },
172 {"-fn", "--tickerfont", "use specified X11 font to draw the ticker", DOString, False, {&config.useX11Font} }
176 ///////////////////////////////////////////////////////////////////////////////
177 // prototypes
179 static void PreparePixmaps( bool freeThemFirst );
180 static void ExitHandler( int sig );
181 static void TimedOut( void );
182 static void CheckTimeOut( bool force );
183 static void CheckMBox( void );
184 static void CheckMaildir( void );
185 static int TraverseDirectory( const char *name, bool isNewMail );
186 static name_t *GetMail( unsigned long checksum );
187 static void UpdatePixmap( bool flashMailSymbol );
188 static void ParseMBoxFile( struct stat *fileStat );
189 static void ParseMaildirFile( const char *fileName, unsigned long checksum,
190 struct stat *fileStat, bool isNewMail );
191 static char *ParseFromField( char *buf );
192 static bool SkipSender( char *address );
193 static void InsertName( char *name, unsigned long checksum, flag_t flag );
194 static void RemoveLastName( void );
195 static void ClearAllNames( void );
196 static void DrawTickerX11Font( void );
197 static void DrawTickerBuildinFont( void );
198 static void ButtonPressed( int button, int state, int x, int y );
199 static void ButtonReleased( int button, int state, int x, int y );
200 static char *XpmColorLine( const char *colorName, char *colorLine,
201 bool disposeLine );
202 static void ReadChecksumFile( void );
203 static void WriteChecksumFile( bool writeAll );
204 static void UpdateChecksum( unsigned long *checksum, const char *buf );
205 static void RemoveChecksumFile( void );
206 static void SetMailFlags( flag_t flag );
207 static void MarkName( unsigned long checksum );
208 static void DetermineState( void );
209 static void UpdateConfiguration( void );
210 static void CleanupNames( void );
211 static bool HasTickerWork( void );
214 ///////////////////////////////////////////////////////////////////////////////
215 // implementation
218 int main( int argc, char **argv )
220 char *usersHome;
221 struct sigaction sa = { .sa_handler = ExitHandler };
222 struct stat fileStat;
223 XTextProperty windowName;
224 char *name = argv[0];
225 DACallbacks callbacks = { NULL, &ButtonPressed, &ButtonReleased,
226 NULL, NULL, NULL, &TimedOut };
228 // read the config file and override the default-settings
229 ReadConfigFile( false );
231 if( config.checksumFileName == NULL ) {
232 if(( usersHome = getenv( "HOME" )) == NULL ) {
233 WARNING( "HOME environment-variable is not set, placing %s in current directory!\n", WMAIL_CHECKSUM_FILE );
234 config.checksumFileName = strdup( WMAIL_CHECKSUM_FILE );
235 } else
236 config.checksumFileName = MakePathName( usersHome, WMAIL_CHECKSUM_FILE );
239 if( config.checksumFileName == NULL )
241 WARNING( "Cannot allocate checksum file-name.\n");
242 exit( EXIT_FAILURE );
245 TRACE( "using checksum-file \"%s\"\n", config.checksumFileName );
247 // parse cmdline-args and override defaults and cfg-file settings
248 DAParseArguments( argc, argv, options,
249 sizeof(options) / sizeof(DAProgramOption),
250 PACKAGE_NAME, PACKAGE_STRING );
252 if( options[0].used )
253 config.givenOptions |= CL_DISPLAY;
254 if( options[1].used )
255 config.givenOptions |= CL_RUNCMD;
256 if( options[2].used )
257 config.givenOptions |= CL_CHECKINTERVAL;
258 if( options[3].used ) {
259 config.givenOptions |= CL_TICKERMODE;
260 config.tickerMode = TICKER_FAMILYNAME;
262 if( options[4].used )
263 config.givenOptions |= CL_FPS;
264 if( options[5].used ) {
265 config.givenOptions |= CL_TICKERMODE;
266 config.tickerMode = TICKER_NICKNAME;
268 if( options[6].used )
269 config.givenOptions |= CL_SYMBOLCOLOR;
270 if( options[7].used )
271 config.givenOptions |= CL_FONTCOLOR;
272 if( options[8].used )
273 config.givenOptions |= CL_BACKCOLOR;
274 if( options[9].used )
275 config.givenOptions |= CL_OFFLIGHTCOLOR;
276 if( options[10].used )
277 config.givenOptions |= CL_BACKGROUNDCOLOR;
278 if( options[11].used ) {
279 config.givenOptions |= CL_NOSHAPE;
280 config.noshape = true;
282 if( options[12].used ) {
283 config.givenOptions |= CL_NEWMAILONLY;
284 config.newMailsOnly = true;
286 if( options[13].used )
287 config.givenOptions |= CL_MAILBOX;
288 if( options[14].used )
289 config.givenOptions |= CL_CMDONMAIL;
290 if( options[15].used ) {
291 config.givenOptions |= CL_CONSIDERSTATUSFIELD;
292 config.considerStatusField = true;
294 if( options[16].used )
295 config.givenOptions |= CL_READSTATUS;
296 if( options[17].used )
297 config.givenOptions |= CL_USEX11FONT;
299 if( config.mailBox == NULL )
300 ABORT( "no mailbox specified - please define at least your $MAIL environment-variable!\n" );
301 else if( stat( config.mailBox, &fileStat ) == 0 )
302 isMaildir = S_ISDIR( fileStat.st_mode ) != 0;
304 TRACE( "mailbox is of type %s\n", isMaildir ? "maildir" : "mbox" );
306 // dockapp size hard wired - sorry...
307 DAInitialize( config.display, "wmail", 64, 64, argc, argv );
309 outPixmap = DAMakePixmap();
310 PreparePixmaps( false );
312 if( sigaction( SIGINT, &sa, NULL ) == -1 ) {
313 perror( "wmail error: sigaction" );
314 exit( EXIT_FAILURE );
317 if( sigaction( SIGTERM, &sa, NULL ) == -1 ) {
318 perror( "wmail error: sigaction" );
319 exit( EXIT_FAILURE );
322 DASetCallbacks( &callbacks );
323 DASetTimeout( 1000 / config.fps );
325 XStringListToTextProperty( &name, 1, &windowName );
326 XSetWMName( DADisplay, DAWindow, &windowName );
328 UpdatePixmap( false );
329 DAShow();
331 DAEventLoop();
333 return 0;
336 static void PreparePixmaps( bool freeMem )
338 // simple recoloring of the raw xpms befor creating Pixmaps of them
339 // this works as long as you don't "touch" the images...
341 #if DA_VERSION < 20030126
342 unsigned dummy;
343 #else
344 unsigned short dummy;
345 #endif
347 XGCValues values;
349 if( config.symbolColor != NULL ) { // symbol color ?
350 symbols_xpm[2] = XpmColorLine( config.symbolColor, symbols_xpm[2],
351 freeMem && ( config.colorsUsed & SYM_COLOR ));
352 config.colorsUsed |= SYM_COLOR;
353 } else {
354 symbols_xpm[2] = XpmColorLine( "#20B2AA", symbols_xpm[2],
355 freeMem && ( config.colorsUsed & SYM_COLOR ));
356 config.colorsUsed |= SYM_COLOR;
359 if( config.fontColor != NULL ) { // font color ?
360 chars_xpm[3] = XpmColorLine( config.fontColor, chars_xpm[3],
361 freeMem && ( config.colorsUsed & FNT_COLOR ));
362 numbers_xpm[3] = XpmColorLine( config.fontColor, numbers_xpm[3],
363 freeMem && ( config.colorsUsed & FNT_COLOR ));
364 config.colorsUsed |= FNT_COLOR;
365 } else {
366 chars_xpm[3] = XpmColorLine( "#D3D3D3", chars_xpm[3],
367 freeMem && ( config.colorsUsed & FNT_COLOR ));
368 numbers_xpm[3] = XpmColorLine( "#D3D3D3", numbers_xpm[3],
369 freeMem && ( config.colorsUsed & FNT_COLOR ));
370 config.colorsUsed |= FNT_COLOR;
373 if( config.backColor != NULL ) { // backlight color ?
374 main_xpm[3] = XpmColorLine( config.backColor, main_xpm[3],
375 freeMem && ( config.colorsUsed & BCK_COLOR ));
376 symbols_xpm[3] = XpmColorLine( config.backColor, symbols_xpm[3],
377 freeMem && ( config.colorsUsed & BCK_COLOR ));
378 chars_xpm[2] = XpmColorLine( config.backColor, chars_xpm[2],
379 freeMem && ( config.colorsUsed & BCK_COLOR ));
380 numbers_xpm[2] = XpmColorLine( config.backColor, numbers_xpm[2],
381 freeMem && ( config.colorsUsed & BCK_COLOR ));
382 config.colorsUsed |= BCK_COLOR;
383 } else {
384 main_xpm[3] = XpmColorLine( "#282828", main_xpm[3],
385 freeMem && ( config.colorsUsed & BCK_COLOR ));
386 symbols_xpm[3] = XpmColorLine( "#282828", symbols_xpm[3],
387 freeMem && ( config.colorsUsed & BCK_COLOR ));
388 chars_xpm[2] = XpmColorLine( "#282828", chars_xpm[2],
389 freeMem && ( config.colorsUsed & BCK_COLOR ));
390 numbers_xpm[2] = XpmColorLine( "#282828", numbers_xpm[2],
391 freeMem && ( config.colorsUsed & BCK_COLOR ));
392 config.colorsUsed |= BCK_COLOR;
395 if( config.offlightColor != NULL ) { // off-light color ?
396 main_xpm[2] = XpmColorLine( config.offlightColor, main_xpm[2],
397 freeMem && ( config.colorsUsed & OFF_COLOR ));
398 numbers_xpm[4] = XpmColorLine( config.offlightColor, numbers_xpm[4],
399 freeMem && ( config.colorsUsed & OFF_COLOR ));
400 config.colorsUsed |= OFF_COLOR;
401 } else {
402 main_xpm[2] = XpmColorLine( "#000000", main_xpm[2],
403 freeMem && ( config.colorsUsed & OFF_COLOR ));
404 numbers_xpm[4] = XpmColorLine( "#000000", numbers_xpm[4],
405 freeMem && ( config.colorsUsed & OFF_COLOR ));
406 config.colorsUsed |= OFF_COLOR;
409 if( config.backgroundColor != NULL ) { // window-frame background (only seen if nonshaped) ?
410 main_xpm[1] = XpmColorLine( config.backgroundColor, main_xpm[1],
411 freeMem && ( config.colorsUsed & BGR_COLOR ));
412 config.colorsUsed |= BGR_COLOR;
415 if( freeMem )
417 XFreePixmap( DADisplay, mainPixmap );
418 XFreePixmap( DADisplay, mainPixmap_mask );
419 XFreePixmap( DADisplay, symbolsPixmap );
420 XFreePixmap( DADisplay, charsPixmap );
421 XFreePixmap( DADisplay, numbersPixmap );
422 XFreePixmap( DADisplay, buttonPixmap );
424 if( tickerGC != NULL )
426 XFreeGC( DADisplay, tickerGC );
427 tickerGC = NULL;
428 if( tickerFS != NULL ) {
429 XFreeFont( DADisplay, tickerFS );
430 tickerFS = NULL;
435 DAMakePixmapFromData( main_xpm, &mainPixmap, &mainPixmap_mask, &dummy, &dummy );
436 DAMakePixmapFromData( symbols_xpm, &symbolsPixmap, NULL, &dummy, &dummy );
437 DAMakePixmapFromData( chars_xpm, &charsPixmap, NULL, &dummy, &dummy );
438 DAMakePixmapFromData( numbers_xpm, &numbersPixmap, NULL, &dummy, &dummy );
439 DAMakePixmapFromData( button_xpm, &buttonPixmap, NULL, &dummy, &dummy );
441 if( config.useX11Font != NULL )
443 XRectangle clipRect;
445 if( config.fontColor != NULL )
446 values.foreground = DAGetColor( config.fontColor );
447 else
448 values.foreground = DAGetColor( "#D3D3D3" );
450 tickerFS = XLoadQueryFont( DADisplay, config.useX11Font );
451 if( tickerFS == NULL )
452 ABORT( "Cannot load font \"%s\"", config.useX11Font );
454 values.font = tickerFS->fid;
455 tickerGC = XCreateGC( DADisplay, DAWindow, GCForeground | GCFont,
456 &values );
457 clipRect.x = 6;
458 clipRect.y = 19;
459 clipRect.width = 52;
460 clipRect.height = 23;
462 XSetClipRectangles( DADisplay, tickerGC, 0, 0, &clipRect, 1, Unsorted );
465 if( config.noshape ) // non-shaped dockapp ?
466 DASetShape( None );
467 else
468 DASetShape( mainPixmap_mask );
471 static void MarkName( unsigned long checksum )
473 name_t *name;
475 for( name = names; name != NULL; name = name->next ) {
476 if( name->checksum == checksum ) {
477 name->flag |= FLAG_READ;
478 if( config.newMailsOnly )
479 numMails--;
480 break;
485 static void DetermineState( void )
487 name_t *name;
489 for( name = names; name != NULL; name = name->next )
490 if(!( name->flag & FLAG_READ )) {
491 state = STATE_NEWMAIL;
493 if( config.cmdOnMail != NULL ) {
494 int ret = system( config.cmdOnMail );
496 if( ret == 127 || ret == -1 )
497 WARNING( "execution of command \"%s\" failed.\n", config.cmdOnMail );
500 break;
504 static void ReadChecksumFile( void )
506 FILE *f = fopen( config.checksumFileName, "rb" );
507 if( f != NULL )
508 while( !feof( f )) {
509 unsigned long checksum;
510 if( fread( &checksum, sizeof(long), 1, f ) != 1 )
511 continue;
513 MarkName( checksum );
515 else
516 return;
518 fclose( f );
521 static void WriteChecksumFile( bool writeAll )
523 FILE *f;
524 TRACE( "writing checksums:" );
526 if(( f = fopen( config.checksumFileName, "wb" )) != NULL ) {
527 name_t *name;
528 for( name = names; name != NULL; name = name->next ) {
529 if( writeAll || (name->flag & FLAG_READ)) {
530 fwrite( &name->checksum, sizeof(long), 1, f );
531 TRACE( " %X", name->checksum );
534 } else
535 return;
537 TRACE( "\n" );
539 fclose( f );
542 static void UpdateChecksum( unsigned long *checksum, const char *buf )
544 if( buf != NULL ) {
545 size_t i, len = strlen( buf );
547 for( i = 0; i < len; ++i )
548 *checksum += buf[i] << (( i % sizeof(long) ) * 8 );
552 static void RemoveChecksumFile( void )
554 TRACE( "removing checksum-file\n" );
555 remove( config.checksumFileName );
558 static void ExitHandler( int sig )
560 (void) sig;
561 caughtSig = 1;
564 static void TimedOut( void )
566 if( caughtSig ) {
567 ClearAllNames();
568 ResetConfigStrings();
569 exit( EXIT_SUCCESS );
572 CheckTimeOut( true );
575 static void CheckTimeOut( bool force )
577 static int checkMail = 0;
579 struct timeval now;
580 gettimeofday(&now, NULL);
582 unsigned long nowMs = now.tv_sec * 1000UL + now.tv_usec / 1000UL;
584 if( !force && nowMs - lastTimeOut < 1000UL / config.fps )
585 return;
587 lastTimeOut = nowMs;
589 if( readConfigFile ) {
590 readConfigFile = false;
591 UpdateConfiguration();
592 checkMail = 0;
593 forceRead = true;
596 if( checkMail == 0 )
598 TRACE( "checking for new mail...\n" );
600 if( isMaildir )
601 CheckMaildir();
602 else
603 CheckMBox();
606 UpdatePixmap( checkMail % config.fps < config.fps/2 );
608 if( ++checkMail >= config.fps * config.checkInterval )
609 checkMail = 0;
612 static void CheckMBox( void )
614 struct stat fileStat;
616 // error retrieving file-stats -> no/zero-size file and no new/read mails
617 // available
618 if( stat( config.mailBox, &fileStat ) == -1 || fileStat.st_size == 0 ) {
619 if( state != STATE_NOMAIL ) {
620 state = STATE_NOMAIL;
621 ClearAllNames();
622 RemoveChecksumFile();
623 forceRedraw = true;
625 } else {
626 // file has changed -> new mails arrived or some mails removed
627 if( lastModifySeconds != fileStat.st_mtime || forceRead ) {
628 forceRead = false;
629 ParseMBoxFile( &fileStat );
630 stat( config.mailBox, &fileStat );
631 forceRedraw = true;
632 // file has accessed (read) -> mark all mails as "read", because
633 // it cannot be decided which mails the user has read...
634 } else if( lastAccessSeconds != fileStat.st_atime ) {
635 state = STATE_READMAIL;
636 WriteChecksumFile( true );
637 if( config.newMailsOnly ) {
638 numMails = 0;
639 SetMailFlags( FLAG_READ );
641 forceRedraw = true;
644 lastModifySeconds = fileStat.st_mtime;
645 lastAccessSeconds = fileStat.st_atime;
649 static void CheckMaildir( void )
651 DIR *dir = NULL;
652 mail_state_t lastState = state;
653 unsigned lastMailCount = numMails;
655 if( forceRead ) {
656 forceRead = false;
657 ClearAllNames();
658 TRACE( "all names cleared\n" );
661 state = STATE_NOMAIL;
663 if(( dir = opendir( config.mailBox )) != NULL )
665 struct dirent *dirEnt = NULL;
666 //bool writeChecksums = false;
667 name_t *name;
669 for( name = names; name != NULL; name = name->next )
670 name->visited = false;
672 while(( dirEnt = readdir( dir )) != NULL )
674 char *fullName = MakePathName( config.mailBox, dirEnt->d_name );
675 struct stat fileStat;
677 if( fullName == NULL )
679 WARNING( "Cannot allocate file/path\n" );
680 break;
683 if( stat( fullName, &fileStat ) == -1 )
684 WARNING( "Can't stat file/path \"%s\"\n", fullName );
685 else if( S_ISDIR( fileStat.st_mode ))
687 if( strcmp( dirEnt->d_name, "new" ) == 0 ) {
688 if( TraverseDirectory( fullName, true ) > 0 )
689 state = STATE_NEWMAIL;
691 else if( strcmp( dirEnt->d_name, "cur" ) == 0 ) {
692 if( TraverseDirectory( fullName, false ) > 0 )
693 if( state != STATE_NEWMAIL )
694 state = STATE_READMAIL;
696 // directories ".", ".." and "tmp" discarded
698 free( fullName );
700 closedir( dir );
701 CleanupNames();
702 } else
703 WARNING( "can't open directory \"%s\"\n", config.mailBox );
705 if( lastState != state || lastMailCount != numMails )
706 forceRedraw = true;
709 static int TraverseDirectory( const char *name, bool isNewMail )
711 DIR *dir = NULL;
712 int mails = 0;
714 if(( dir = opendir( name )) != NULL )
716 struct dirent *dirEnt = NULL;
718 while(( dirEnt = readdir( dir )) != NULL )
720 char *fullName = MakePathName( name, dirEnt->d_name );
721 struct stat fileStat;
722 unsigned long checksum = 0;
723 name_t *name;
725 if( fullName == NULL )
727 WARNING( "Cannot allocate file/path\n" );
728 break;
731 if( stat( fullName, &fileStat ) == -1 )
732 WARNING( "Can't stat file/path \"%s\"\n", fullName );
733 else if( !S_ISDIR( fileStat.st_mode ))
735 TRACE( "found email-file \"%s\"\n", fullName );
736 UpdateChecksum( &checksum, dirEnt->d_name );
738 if(( name = GetMail( checksum )) == NULL )
740 TRACE( "-> new file - parsing it\n" );
741 ParseMaildirFile( fullName, checksum, &fileStat, isNewMail );
743 else {
744 name->flag = isNewMail ? FLAG_INITIAL : FLAG_READ;
745 name->visited = true;
747 ++mails;
749 free( fullName );
753 closedir( dir );
755 return mails;
758 static name_t *GetMail( unsigned long checksum )
760 name_t *name;
762 for( name = names; name != NULL; name = name->next )
763 if( name->checksum == checksum )
764 return name;
766 return NULL;
769 static void UpdatePixmap( bool flashMailSymbol )
771 int drawCount, i;
773 if( !forceRedraw && !HasTickerWork() )
774 return;
776 forceRedraw = false;
778 XCopyArea( DADisplay, mainPixmap, outPixmap, DAGC,
779 0, 0, 64, 64, 0, 0 );
781 if( numMails > 999 )
783 XCopyArea( DADisplay, numbersPixmap, outPixmap, DAGC,
784 50, 0, 5, 9, 6, 49 );
785 drawCount = 999;
786 } else
787 drawCount = numMails;
789 for( i = 0; i < 3; ++i, drawCount /= 10 )
791 XCopyArea( DADisplay, numbersPixmap, outPixmap, DAGC,
792 (drawCount%10)*5, 0, 5, 9, 24-i*6, 49 );
793 if( drawCount <= 9 )
794 break;
797 if( buttonPressed )
799 XCopyArea( DADisplay, buttonPixmap, outPixmap, DAGC,
800 0, 0, 23, 11, 36, 48 );
803 switch( state ) {
804 case STATE_NEWMAIL:
805 if( flashMailSymbol )
806 XCopyArea( DADisplay, symbolsPixmap, outPixmap, DAGC,
807 13, 0, 37, 12, 20, 7 );
808 /* fall through */
809 case STATE_READMAIL:
810 XCopyArea( DADisplay, symbolsPixmap, outPixmap, DAGC,
811 0, 0, 13, 12, 7, 7 );
813 if( config.useX11Font == NULL )
814 DrawTickerBuildinFont();
815 else
816 DrawTickerX11Font();
817 break;
818 default: // make compiler happy
819 break;
822 DASetPixmap( outPixmap );
825 static void ParseMBoxFile( struct stat *fileStat )
827 char buf[1024];
828 struct utimbuf timeStruct;
829 int fromFound = 0;
830 FILE *f = fopen( config.mailBox, "rt" );
831 unsigned long checksum;
833 state = STATE_READMAIL;
834 ClearAllNames();
836 numMails = 0;
838 if( f == NULL ) {
839 WARNING( "can't open mbox \"%s\"\n", config.mailBox );
840 return;
843 while( fgets( buf, 1024, f ) != NULL )
845 if( strncmp( buf, "From ", 5 ) == 0 ) {
846 fromFound = 1;
847 checksum = 0;
848 continue;
851 if( fromFound )
852 UpdateChecksum( &checksum, buf );
854 if( fromFound && strncasecmp( buf, "from: ", 6 ) == 0 )
856 if( SkipSender( buf+6 ))
857 goto NEXTMAIL;
859 char *name;
860 if(( name = ParseFromField( buf+6 )) == NULL )
862 WARNING( "Could not parse From field\n" );
863 break;
865 InsertName( name, checksum, FLAG_INITIAL );
867 ++numMails;
868 fromFound = 0;
869 checksum = 0;
870 } else if( config.considerStatusField && strncasecmp( buf, "status: ", 8 ) == 0 &&
871 strstr( buf+8, config.readStatus ) == NULL )
873 RemoveLastName();
874 --numMails;
876 NEXTMAIL:
880 fclose( f );
881 ReadChecksumFile();
883 DetermineState();
885 timeStruct.actime = fileStat->st_atime;
886 timeStruct.modtime = fileStat->st_mtime;
887 utime( config.mailBox, &timeStruct );
890 static void ParseMaildirFile( const char *fileName, unsigned long checksum,
891 struct stat *fileStat, bool isNewMail )
893 char buf[1024];
894 struct utimbuf timeStruct;
895 FILE *f = fopen( fileName, "rt" );
897 if( f == NULL )
899 WARNING( "can't open maildir file \"%s\"\n", fileName );
900 return;
903 while( fgets( buf, 1024, f ) != NULL )
905 if( strncasecmp( buf, "from: ", 6 ) == 0 )
907 if( SkipSender( buf+6 ))
908 break;
910 char *name;
911 if(( name = ParseFromField( buf+6 )) == NULL )
913 WARNING( "Could not parse From field\n" );
914 break;
916 InsertName( name, checksum, isNewMail ? FLAG_INITIAL : FLAG_READ );
918 //++numMails;
922 fclose( f );
924 timeStruct.actime = fileStat->st_atime;
925 timeStruct.modtime = fileStat->st_mtime;
926 utime( fileName, &timeStruct );
929 static char *ParseFromField( char *buf )
931 parse_state_t state = STATE_FULLNAME;
932 int fullNameEncoded = 0;
933 int saveAtCharPos = -1;
934 char *fullName;
935 char *addressName;
936 char *atChar = NULL;
937 char *c;
938 size_t maxLen = strlen( buf ) + 1;
939 char *comment;
940 size_t fullNameLen = 0, addressNameLen = 0, commentLen = 0;
942 if(( fullName = calloc( maxLen, sizeof *fullName )) == NULL )
943 return NULL;
944 if(( addressName = calloc( maxLen, sizeof *addressName )) == NULL )
946 free( fullName );
947 return NULL;
949 if(( comment = calloc( maxLen, sizeof *comment )) == NULL )
951 free( fullName );
952 free( addressName );
953 return NULL;
956 // FIXME: Don't do that "encoded" dance. It's not intended by
957 // RFC2047, and it's better to just do it in the end.
958 // Cleaner.
960 for( c = buf; *c != '\0'; ++c )
962 switch( state ) {
963 case STATE_FULLNAME:
965 switch( *c ) {
966 case '"':
967 state = STATE_QUOTED_FULLNAME;
968 continue;
969 case '<':
970 while( fullNameLen > 0 &&
971 isspace( fullName[ fullNameLen - 1 ] ))
972 fullNameLen--;
973 fullName[ fullNameLen ] = '\0';
974 state = STATE_ADDRESS;
975 continue;
976 case '@':
977 saveAtCharPos = fullNameLen;
978 fullName[ fullNameLen++ ] = *c;
979 continue;
980 case '(':
981 state = STATE_COMMENT;
982 continue;
983 case '=':
984 if( *(c+1) == '?' ) {
985 ++c;
986 fullNameEncoded = 1;
987 state = STATE_ENCODED_FULLNAME;
988 continue;
990 /* else fall through */
991 default:
992 if( fullName[0] != '\0' || !isspace( *c ))
993 fullName[ fullNameLen++ ] = *c;
995 continue;
997 case STATE_QUOTED_FULLNAME:
999 switch( *c ) {
1000 case '\\':
1001 fullName[ fullNameLen++ ] = *(++c);
1002 continue;
1003 case '"':
1004 state = STATE_FULLNAME;
1005 continue;
1006 default:
1007 fullName[ fullNameLen++ ] = *c;
1009 continue;
1011 case STATE_ENCODED_FULLNAME:
1013 switch( *c ) {
1014 case '?':
1015 if( *(c+1) == '=' ) {
1016 ++c;
1017 state = STATE_FULLNAME;
1018 continue;
1020 default:
1021 ; // do nothing... COMING SOON: decode at least latin1
1023 continue;
1025 case STATE_ADDRESS:
1027 switch( *c ) {
1028 case '"':
1029 state = STATE_QUOTED_ADDRESS;
1030 case '>':
1031 case '\n':
1032 // FIXME: Shouldn't it break here?
1033 // Since the address is finished?
1034 continue;
1035 case '@':
1036 atChar = addressName + addressNameLen;
1037 addressName[ addressNameLen++ ] = *c;
1038 continue;
1039 default:
1040 addressName[ addressNameLen++ ] = *c;
1042 continue;
1044 case STATE_QUOTED_ADDRESS:
1046 switch( *c ) {
1047 case '"':
1048 state = STATE_ADDRESS;
1049 continue;
1050 case '\\':
1051 addressName[ addressNameLen++ ] = *(++c);
1052 continue;
1053 default:
1054 addressName[ addressNameLen++ ] = *c;
1056 continue;
1057 case STATE_COMMENT:
1058 switch( *c ) {
1059 case ')':
1060 state = STATE_FULLNAME;
1061 continue;
1062 default:
1063 comment[ commentLen++ ] = *c;
1064 continue;
1066 continue;
1070 if( *comment ) {
1071 //WARNING("Comment seen: %s\nIn: %s\nFullname: %s\nAddress: %s\n", comment, buf, fullName, addressName);
1072 // Comment seen: if there's an address, append to
1073 // fullname. If no address, copy fullname to address
1074 // and comment to fullname.
1075 if( *addressName ) {
1076 strcat(fullName, "(");
1077 strcat(fullName, comment);
1078 strcat(fullName, ")");
1079 } else {
1080 strcpy(addressName, fullName);
1081 strcpy(fullName, comment);
1084 free( comment );
1086 //WARNING("Fullname: %s\nAddress: %s\n", fullName, addressName);
1088 // what name should be tickered
1089 if( config.tickerMode == TICKER_FAMILYNAME && fullName[0] != '\0' && !fullNameEncoded ) {
1090 free( addressName );
1091 return fullName;
1092 } else {
1093 if( state == STATE_FULLNAME ) {
1094 strcpy( addressName, fullName );
1095 if( saveAtCharPos != -1 )
1096 atChar = &addressName[saveAtCharPos];
1098 if( config.tickerMode == TICKER_NICKNAME ) {
1099 if( atChar != NULL )
1100 *atChar = '\0';
1102 free( fullName );
1103 return addressName;
1107 static bool SkipSender( char *address )
1109 char **skipName;
1110 size_t len = strlen( address );
1112 // remove trailing '\n' got from fgets
1113 if( address[len-1] == '\n' )
1114 address[len-1] = '\0';
1116 for( skipName = config.skipNames;
1117 skipName != NULL && *skipName != NULL; skipName++ )
1119 TRACE( "comparing \"%s\" and \"%s\"\n", *skipName, address );
1121 // call libc-fnmatch (wildcard-match :-) !
1122 if( !fnmatch( *skipName, address, 0 )) {
1123 TRACE( "skipping sender \"%s\"\n", *skipName );
1124 return true;
1128 return false;
1131 static void InsertName( char *name, unsigned long checksum, flag_t flag )
1133 name_t *item;
1135 TRACE( "insertName: %X, \"%s\"\n", checksum, name );
1136 if(( item = malloc( sizeof( name_t ))) == NULL )
1138 free( name );
1139 return;
1142 item->name = name; /*strdup( name );*/
1143 item->checksum = checksum;
1144 item->flag = flag;
1145 item->visited = true;
1146 item->next = names;
1147 names = item;
1149 namesChanged = true;
1152 static void RemoveLastName( void )
1154 if( names != NULL ) {
1155 name_t *name = names;
1156 names = names->next;
1157 free( name->name );
1158 free( name );
1162 static void ClearAllNames( void )
1164 name_t *name, *nextName;
1166 for( name = names; name != NULL; name = nextName ) {
1167 nextName = name->next;
1169 free( name->name );
1170 free( name );
1173 names = NULL;
1174 numMails = 0;
1176 namesChanged = true;
1179 static void SetMailFlags( flag_t flag )
1181 name_t *name;
1183 for( name = names; name != NULL; name = name->next )
1184 name->flag |= flag;
1187 static void DrawTickerX11Font( void )
1189 // 49x21+7+20 out-drawable size
1191 static int insertAt;
1193 if( curTickerName == NULL || namesChanged )
1195 for( curTickerName = names;
1196 curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ );
1197 curTickerName = curTickerName->next );
1199 if( curTickerName == NULL )
1200 return;
1202 namesChanged = false;
1203 insertAt = 54;
1206 XDrawString( DADisplay, outPixmap, tickerGC, insertAt,
1207 41-tickerFS->max_bounds.descent,
1208 curTickerName->name, strlen( curTickerName->name ));
1210 --insertAt;
1212 if( insertAt < -XTextWidth( tickerFS, curTickerName->name,
1213 strlen( curTickerName->name )) + 6 )
1215 do {
1216 curTickerName = curTickerName->next;
1217 } while( curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ ));
1219 if( curTickerName != NULL ) {
1220 insertAt = 54;
1225 static void DrawTickerBuildinFont( void )
1227 // 49x21+7+20 out-drawable size
1228 // 14x21 font-character size
1230 static unsigned insertAt;
1231 static unsigned takeItFrom;
1233 unsigned leftSpace;
1234 unsigned drawTo;
1235 unsigned char *currentChar;
1237 if( names == NULL )
1238 return;
1240 if( curTickerName == NULL || namesChanged ) {
1242 for( curTickerName = names;
1243 curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ );
1244 curTickerName = curTickerName->next );
1246 if( curTickerName == NULL )
1247 return;
1249 insertAt = 57;
1250 takeItFrom = 0;
1251 namesChanged = false;
1254 leftSpace = takeItFrom % 14;
1256 for( currentChar = (unsigned char *)&curTickerName->name[takeItFrom/14],
1257 drawTo = insertAt; *currentChar != '\0'; ++currentChar )
1260 int outChar = (*currentChar < 32 || *currentChar >= 128) ? '?' :
1261 *currentChar;
1262 int charWidth = 57-drawTo >= 14 ? 14 - leftSpace : 57-drawTo;
1264 XCopyArea( DADisplay, charsPixmap, outPixmap, DAGC,
1265 (outChar-32)*14+leftSpace, 0, charWidth, 21, drawTo, 20 );
1267 leftSpace = 0;
1268 drawTo += charWidth;
1270 if( drawTo > 57 )
1271 break;
1274 if( --insertAt < 7 ) {
1275 insertAt = 7;
1276 takeItFrom++;
1278 if( takeItFrom/14 >= strlen( curTickerName->name )) {
1280 do {
1281 curTickerName = curTickerName->next;
1282 } while( curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ ));
1284 if( curTickerName != NULL ) {
1285 takeItFrom = 0;
1286 insertAt = 57;
1292 static void ButtonPressed( int button, int state, int x, int y )
1294 (void) button;
1295 (void) state;
1297 if( x >= 35 && x <= 59 && y >= 47 && y <= 59 ) {
1298 buttonPressed = true;
1299 forceRedraw = true;
1300 } else
1301 // reread the config file
1302 readConfigFile = true;
1304 CheckTimeOut( false );
1307 static void ButtonReleased( int button, int state, int x, int y )
1309 (void) button;
1310 (void) state;
1312 buttonPressed = false;
1313 forceRedraw = true;
1315 if( x >= 35 && x <= 59 && y >= 47 && y <= 59 ) {
1316 int ret = system( config.runCmd );
1318 if( ret == 127 || ret == -1 )
1319 WARNING( "execution of command \"%s\" failed.\n", config.runCmd );
1322 CheckTimeOut( false );
1325 static void GetHexColorString( const char *colorName, char *xpmLine )
1327 XColor color;
1329 if( XParseColor( DADisplay,
1330 DefaultColormap( DADisplay, DefaultScreen( DADisplay )),
1331 colorName, &color ))
1333 sprintf( xpmLine, "%02X%02X%02X", color.red>>8, color.green>>8,
1334 color.blue>>8 );
1335 } else
1336 WARNING( "unknown colorname: \"%s\"\n", colorName );
1339 static char *XpmColorLine( const char *colorName, char *colorLine,
1340 bool disposeLine )
1342 char *newLine = strdup( colorLine );
1343 char *from = strrchr( newLine, '#' );
1345 if( from == NULL && !strcasecmp( &colorLine[ strlen( colorLine ) - 4 ], "none" )) {
1346 // if no # found, it should be a None-color line
1347 free( newLine );
1348 newLine = malloc( 12 );
1349 strcpy( newLine, " \tc #" );
1350 newLine[11] = '\0';
1351 from = newLine + 4;
1354 if( disposeLine )
1355 free( colorLine );
1357 GetHexColorString( colorName, from+1 );
1359 return newLine;
1362 static void UpdateConfiguration( void )
1364 struct stat fileStat;
1366 TRACE( "reading configuration file...\n" );
1368 ReadConfigFile( true );
1370 // if no path/name to an mbox or maildir inbox directory was given,
1371 // use the environment
1372 if( config.mailBox == NULL )
1373 config.mailBox = getenv( "MAIL" );
1375 // mbox or maildir ?
1376 if( config.mailBox != NULL && stat( config.mailBox, &fileStat ) == 0 )
1377 isMaildir = S_ISDIR( fileStat.st_mode ) != 0;
1378 else
1379 isMaildir = false;
1381 TRACE( "mailbox is of type %s\n", isMaildir ? "maildir" : "mbox" );
1383 PreparePixmaps( true );
1385 DASetTimeout( 1000 / config.fps );
1388 static void CleanupNames( void )
1390 name_t *name, *last = NULL, *nextName;
1392 numMails = 0;
1394 for( name = names; name != NULL; name = nextName )
1396 nextName = name->next;
1398 if( !name->visited ) {
1399 if( last == NULL )
1400 names = name->next;
1401 else
1402 last->next = name->next;
1404 free( name->name );
1405 free( name );
1406 } else {
1407 last = name;
1409 if( !config.newMailsOnly || (name->flag & FLAG_READ) == 0 )
1410 ++numMails;
1415 static bool HasTickerWork( void )
1417 name_t *nextTickerName;
1419 if( names == NULL )
1420 return false;
1422 if( curTickerName == NULL || namesChanged ) {
1424 for( nextTickerName = names;
1425 nextTickerName != NULL && ( config.newMailsOnly && ( nextTickerName->flag & FLAG_READ ));
1426 nextTickerName = nextTickerName->next );
1428 if( nextTickerName == NULL )
1429 return false;
1432 return true;