wmbiff: parse IMAP configuration on FreeBSD
[dockapps.git] / wmymail / wmymail.c
blobdfe045fa6e4021c6f2c673119ff478d1a699fa56
1 /* wmymail.c - mail checking dockapp */
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <signal.h>
7 #include <utime.h>
8 #include <sys/time.h>
9 #include <sys/stat.h>
10 #include <unistd.h>
11 #include <X11/Xlib.h>
12 #include <libdockapp/dockapp.h>
14 #include "xpm/main.xpm"
15 #include "xpm/numbers.xpm"
16 #include "xpm/unumbers.xpm"
17 #include "xpm/mbox_1.xpm"
18 #include "xpm/mbox_2.xpm"
19 #include "xpm/mbox_3.xpm"
22 * Some definitions required by libdockapp
25 #define NAME "wmymail"
26 #define VERSION "wmymail v0.3 November 6, 2004"
28 // default mail check interval, in seconds (default) or minutes (with -F)
29 #define CHECKINTERVAL 1
31 // global data
33 char *displayName = "";
34 char *mailPath = NULL;
35 char *fontColor = "";
36 char *background = "";
37 char *clickcommand = "";
38 char *newcommand = "";
39 int numMessages = 0;
40 int lastnumMessages = 0;
41 int numRead = 0;
42 int numUnread = 0;
43 int lastnumUnread = 0;
44 int buttonpressed = 0;
47 * usefetchmail means to run "fetchmail -c" and parse the output, rather
48 * than counting up messages in an mbox file.
50 * It will change the interval from seconds to minutes.
53 int usefetchmail = 0;
54 int flip = 1;
55 int checkInterval = CHECKINTERVAL;
56 time_t lastModifySeconds = 0;
57 off_t lastSize = 0;
59 Pixmap mainPixmap;
60 Pixmap numbersPixmap;
61 Pixmap unumbersPixmap;
62 Pixmap mboxonePixmap;
63 Pixmap mboxtwoPixmap;
64 Pixmap mboxthreePixmap;
65 Pixmap outPixmap1;
66 Pixmap outPixmap2;
67 GC defaultGC;
69 static DAProgramOption options[] = {
71 {"-display", NULL, "display to use",
72 DOString, False, {&displayName}},
74 {"-i", "--interval", "seconds between mailbox checks (default 1)",
75 DONatural, False, {&checkInterval} },
77 {"-fc", "--fontcolor", "custom font color",
78 DOString, False, {&fontColor} },
80 {"-bg", "--background", "custom background color for non-shaped window",
81 DOString, False, {&background} },
83 {"-ns", "--noshape", "make the dock app non-shaped (windowed)",
84 DONone, False, {NULL} },
86 {"-F", "--fetchmail", "check with fetchmail -c instead of the mbox",
87 DONone, False, {NULL} },
89 {"-c", "--command", "command to run when clicked",
90 DOString, False, {&clickcommand} },
92 {"-n", "--newcommand", "command to run when new mail is received",
93 DOString, False, {&newcommand} },
95 {"-m", "--mailbox", "mailbox to use when $MAIL is not set",
96 DOString, False, {&mailPath} }
100 // prototypes
102 void checkForNewMail(int dummy);
103 void updatePixmap(void);
104 void parseMailFile( struct stat *fileStat );
105 char *getHexColorString( char *colorName );
106 void putnumber (int number, Pixmap pixmap, Pixmap numbers,
107 int destx, int desty);
108 void buttonpress(int button, int state, int x, int y);
109 void buttonrelease(int button, int state, int x, int y);
110 void checkfetchmail (void);
111 void checkmbox (void);
112 void launch (const char *command);
114 // functions
116 int main(int argc, char **argv) {
117 Pixmap mainPixmap_mask;
119 unsigned width, height;
121 DACallbacks callbacks = { NULL, &buttonpress, &buttonrelease,
122 NULL, NULL, NULL, NULL };
124 struct sigaction sa;
126 sa.sa_handler = SIG_IGN;
127 #ifdef SA_NOCLDWAIT
128 sa.sa_flags = SA_NOCLDWAIT;
129 #else
130 sa.sa_flags = 0;
131 #endif
132 sigemptyset(&sa.sa_mask);
133 sigaction(SIGCHLD, &sa, NULL);
136 DAParseArguments(argc, argv, options,
137 sizeof(options) / sizeof(DAProgramOption),
138 NAME, VERSION);
140 DAInitialize(displayName, "wmymail", 64, 64, argc, argv);
142 // simple recoloring of the raw xpms befor creating Pixmaps of them
143 // this works as long as you don't "touch" the images...
145 if (options[2].used) { // custom font color ?
146 char *colorLine = strdup("+ c #");
148 strcat(colorLine, getHexColorString(fontColor));
150 colorLine = strdup("+ c #");
151 strcat(colorLine, getHexColorString(fontColor));
152 numbers_xpm[3] = colorLine;
155 if (options[3].used && options[8].used) { // custom window background ?
156 char *colorLine = strdup(" c #");
158 strcat(colorLine, getHexColorString(background));
159 main_xpm[1] = colorLine;
162 DAMakePixmapFromData(main_xpm, &mainPixmap, &mainPixmap_mask, &width, &height);
163 DAMakePixmapFromData(numbers_xpm, &numbersPixmap, NULL, &width, &height);
164 DAMakePixmapFromData(unumbers_xpm, &unumbersPixmap, NULL, &width, &height);
166 DAMakePixmapFromData(mbox_1_xpm, &mboxonePixmap, NULL, &width, &height);
167 DAMakePixmapFromData(mbox_2_xpm, &mboxtwoPixmap, NULL, &width, &height);
168 DAMakePixmapFromData(mbox_3_xpm, &mboxthreePixmap, NULL, &width, &height);
170 if (!options[4].used) // no shape to install
171 DASetShape(mainPixmap_mask);
173 if (options[5].used) // use fetchmail
174 usefetchmail = 1;
175 else if (mailPath == NULL) {
176 if ((mailPath = getenv("MAIL")) == NULL) {
177 perror("Please define your MAIL environment variable!\n");
178 exit(1);
182 DASetCallbacks( &callbacks );
183 DASetTimeout(-1);
185 outPixmap1 = DAMakePixmap();
186 outPixmap2 = DAMakePixmap();
187 defaultGC = XDefaultGC(DADisplay, 0);
189 signal(SIGALRM, checkForNewMail);
191 updatePixmap();
193 DAShow();
195 checkForNewMail(0);
197 DAEventLoop();
199 return 0;
202 char *getHexColorString(char *colorName) {
203 XColor color;
204 char *hexColorString;
206 if (!XParseColor(DADisplay,
207 DefaultColormap(DADisplay, DefaultScreen( DADisplay)),
208 colorName, &color))
210 printf("unknown colorname: \"%s\"\n", colorName);
211 exit(1);
214 hexColorString = (char *)malloc(7);
215 sprintf(hexColorString, "%02X%02X%02X", color.red>>8, color.green>>8,
216 color.blue>>8);
218 return hexColorString;
223 * checkForNewMail
227 void checkForNewMail(int dummy) {
228 struct itimerval timerVal;
230 if (usefetchmail) {
231 checkfetchmail();
232 } else {
233 checkmbox();
236 if (numMessages != lastnumMessages ||
237 numUnread != lastnumUnread) {
238 updatePixmap();
239 if (numUnread > lastnumUnread && strlen(newcommand) > 0)
240 launch(newcommand);
241 lastnumMessages = numMessages;
242 lastnumUnread = numUnread;
245 memset(&timerVal, 0, sizeof(timerVal));
247 if (usefetchmail) {
248 timerVal.it_value.tv_sec = checkInterval * 60;
249 } else {
250 timerVal.it_value.tv_sec = checkInterval;
253 setitimer(ITIMER_REAL, &timerVal, NULL);
258 * checkfetchmail
262 void checkfetchmail (void) {
263 int msgtotal = 0;
264 int msgseen = 0;
265 int snpret;
266 char tmpfile[20] = "wmymail.XXXXXX";
267 char syscmd[120];
268 char line[1024];
269 char *s, *t;
270 int fd;
271 FILE *f;
274 fd = mkstemp(tmpfile);
275 if (fd == -1) {
276 perror("wmymail: cannot get a temporay file");
277 return;
280 snpret = snprintf(syscmd, 120, "fetchmail -c > %s", tmpfile);
281 if (snpret < 0) {
282 perror("wmymail: error in snprintf() call (should not happen)");
283 return;
286 if (system(syscmd) < 0) {
287 perror("wmymail: error when using system() to run fetchmail -c");
288 return;
291 f = fdopen(fd, "r");
292 if (f == NULL) {
293 perror("wmymail: can't reread tempfile\n");
294 return;
297 /* FIXME: this assumes that fetchmail will never print a line over
298 * 1024 characters long, which is fairly safe but you never know */
299 while (fgets(line, 1024, f) != NULL) {
301 /* Every line beginning with a number is assumed to be a number of
302 * messages on the server:
304 * "1 message for userfoo at mail.bar.org."
305 * "3 messages for userfoo at mail.bar.org." */
306 if (line[0] >= '0' && line[0] <= '9') {
308 /* The first number on the line may be added to the total */
309 msgtotal += atoi(line);
311 /* Fetchmail may also indicate that some of the messages on the
312 * server have already been read:
314 * "5 messages (3 seen) for userfoo at mail.bar.org." */
316 /* To get the number seen, locate the first space */
317 s = (char *)strstr(line, " ");
318 if (s != NULL) {
320 /* Skip over one character */
321 s++;
323 /* And locate the second space */
324 t = (char *)strstr(s, " ");
326 /* If this second space is followed by '(' and a digit, it's
327 * a number of seen messages */
328 if (t != NULL && t[1] == '(' && t[2] >= '0' && t[2] <= '9') {
330 /* Position string t on the number seen */
331 t += 2;
333 /* And get the number */
334 msgseen += atoi(t);
340 fclose(f);
341 remove(tmpfile);
343 /* Now that that's been gotten through without major errors,
344 move the values to the global variables */
346 numMessages = msgtotal;
347 numUnread = msgtotal - msgseen;
351 * checkmbox
355 void checkmbox (void) {
356 struct stat fileStat;
358 if (stat(mailPath, &fileStat) == -1 || fileStat.st_size == 0) {
359 numMessages = 0;
360 numUnread = 0;
361 } else if (lastModifySeconds != fileStat.st_mtime ||
362 lastSize != fileStat.st_size) {
364 parseMailFile(&fileStat);
366 lastModifySeconds = fileStat.st_mtime;
367 lastSize = fileStat.st_size;
373 * updatePixmap
377 void updatePixmap(void) {
378 Pixmap outPixmap = flip ? outPixmap1 : outPixmap2;
380 flip = !flip;
382 XCopyArea(DADisplay, mainPixmap, outPixmap, defaultGC,
383 0, 0, 64, 64, 0, 0);
385 if (numMessages > 998) {
386 putnumber(999, outPixmap, numbersPixmap, 40, 49);
387 } else {
388 putnumber(numMessages, outPixmap, numbersPixmap, 40, 49);
391 if (numUnread > 998) {
392 putnumber(999, outPixmap, unumbersPixmap, 6, 49);
393 } else if (!numUnread) {
394 putnumber(0, outPixmap, numbersPixmap, 6, 49);
395 } else {
396 putnumber(numUnread, outPixmap, unumbersPixmap, 6, 49);
399 if (numUnread == 0) {
400 // do nothing.
401 } else if (numUnread == 1) {
402 XCopyArea(DADisplay, mboxonePixmap, outPixmap, defaultGC,
403 0, 0, 40, 34, 14, 6);
404 } else if (numUnread == 2) {
405 XCopyArea(DADisplay, mboxtwoPixmap, outPixmap, defaultGC,
406 0, 0, 40, 34, 14, 6);
407 } else {
408 XCopyArea(DADisplay, mboxthreePixmap, outPixmap, defaultGC,
409 0, 0, 40, 34, 14, 6);
412 DASetPixmap(outPixmap);
417 * putnumber -- draw a number
421 void putnumber (
422 int number, /* what value should be displayed */
423 Pixmap pixmap, /* pixmap to draw upon */
424 Pixmap numbers, /* pixmap with digit images to use */
425 int destx, int desty /* upper-left corner of rectangle to draw in */
428 int digit1, digit2, digit3;
430 /* Determine the digits */
431 digit1 = number / 100;
432 digit2 = (number % 100) / 10;
433 digit3 = number % 10;
435 /* The 100s and 10s digits will only be displayed if the number
436 is >99 and >9, respectively */
438 if (digit1) XCopyArea(DADisplay, numbers, pixmap, defaultGC,
439 digit1 * 5, 0, 5, 9, destx, desty);
441 if (digit2 || digit1) XCopyArea(DADisplay, numbers, pixmap, defaultGC,
442 digit2 * 5, 0, 5, 9, destx + 6, desty);
444 XCopyArea(DADisplay, numbers, pixmap, defaultGC,
445 digit3 * 5, 0, 5, 9, destx + 12, desty);
449 * parseMailFile -- reads the mail file and sets the global variables:
451 * numMessages -- total number of messages (displayed on the right)
452 * numRead -- messages that have been read
453 * numUnread -- message not yet read (displayed on the left)
456 void parseMailFile(struct stat *fileStat) {
457 char buf[1024];
458 int inHeader = 0;
459 int statusRead = 0;
460 int longline = 0;
461 FILE *f = fopen(mailPath, "r"); /* FIXME check for failure to open */
463 numMessages = 0;
464 numRead = 0;
466 while (fgets(buf, 1024, f) != NULL) {
468 /* Keep discarding data if a line over 1024 characters long was found */
469 if (longline) {
470 longline = index(buf, '\n') != NULL;
472 } else {
473 /* The "From" line is the marker of an individual message */
474 if(!strncmp(buf, "From ", 5)) {
475 inHeader = 1;
476 numMessages++;
478 /* Once inside a header, it only remains to
479 * 1) Take note, if the message appears to have been read
480 * 2) Locate the end of the header */
481 } else if (inHeader) {
483 /* A blank line indicates the end of the header */
484 if (!strcmp(buf, "\n")) {
485 inHeader = 0;
486 if (statusRead) {
487 numRead++;
488 statusRead = 0;
491 /* The "Status" line indicates that the message has been read,
492 * if it has a "R". But since we don't trust that there will
493 * be only one "Status" line, statusRead will be set to 1,
494 * but numRead will only be incremented after the header has
495 * been completely read. That way, multiple "Status" lines
496 * would only set statusRead to 1 multiple times (having no
497 * effect). */
498 } else if (!strncmp(buf, "Status: ", 8) && strchr(buf, 'R')) {
499 statusRead = 1;
503 /* The 1024 byte buffer can easily be exceeded by long lines...
504 * when no newline is present, we must enter the state of "skipping
505 * over the rest of a very long line". Else a line inside the body
506 * of a message might be (starting at the 1025th character)
507 * "From <foo@bar.org>\n" thus fooling this program into parsing it
508 * incorrectly. */
509 longline = index(buf, '\n') == NULL;
513 fclose(f);
514 numUnread = numMessages - numRead;
517 /* Take note of a mouse button being pressed inside the dock app */
518 void buttonpress (int button, int state, int x, int y) {
519 buttonpressed = 1;
522 /* A mouse button was pressed and released.
523 * See if it was released while the mouse was still in the bounds of
524 * the dock app (a 64x64 square). */
525 void buttonrelease (int button, int state, int x, int y) {
526 if (buttonpressed && x > 0 && x < 64 && y > 0 && y < 64 &&
527 strlen(clickcommand) > 0) {
528 launch(clickcommand);
530 buttonpressed = 0;
533 /* Start another program */
534 void launch (const char *command) {
535 int cpid;
537 cpid = fork();
538 if (cpid == -1) {
539 perror("can't fork");
540 } else if (cpid == 0) {
541 system(command);
542 exit(0);