///////////////////////////////////////////////////////////////////////////////
// wmail.c
// email indicator tool designed as docklet for Window Maker
// main c source-file
//
// wmail version 2.0
//
// Copyright 2000~2002, Sven Geisenhainer <sveng@informatik.uni-jena.de>.
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions, and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions, and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
// 3. The name of the author may not be used to endorse or promote products
//    derived from this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


///////////////////////////////////////////////////////////////////////////////
// includes

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <utime.h>
#include <fnmatch.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <dirent.h>
#include <X11/Xlib.h>
#include <dockapp.h>
#include "common.h"
#include "config.h"

// pixmaps
#ifdef USE_DELT_XPMS
#include "xpm_delt/main.xpm"
#include "xpm_delt/symbols.xpm"
#include "xpm_delt/numbers.xpm"
#include "xpm_delt/button.xpm"
#include "xpm_delt/chars.xpm"
#else
#include "xpm/main.xpm"
#include "xpm/symbols.xpm"
#include "xpm/numbers.xpm"
#include "xpm/button.xpm"
#include "xpm/chars.xpm"
#endif


///////////////////////////////////////////////////////////////////////////////
// typedefs

typedef enum {
    FLAG_INITIAL = 0,
    FLAG_READ    = 1
} flag_t;

typedef struct _name_t {
    char *name;
    unsigned long checksum;
    flag_t flag;
	bool visited;
    struct _name_t *next;
} name_t;

typedef enum {
    STATE_NOMAIL,
    STATE_NEWMAIL,
    STATE_READMAIL
} mail_state_t;

typedef enum {
    STATE_ADDRESS,
    STATE_QUOTED_ADDRESS,
    STATE_FULLNAME,
    STATE_QUOTED_FULLNAME,
    STATE_ENCODED_FULLNAME,
	STATE_COMMENT
} parse_state_t;


///////////////////////////////////////////////////////////////////////////////
// data

mail_state_t state = STATE_NOMAIL;
int numMails = 0;
bool namesChanged = false;
bool buttonPressed = false;
bool readConfigFile = false;
bool isMaildir = false;
bool forceRead = false;
bool forceRedraw = true;
time_t lastModifySeconds = 0;
time_t lastAccessSeconds = 0;
Pixmap mainPixmap;
Pixmap mainPixmap_mask;
Pixmap symbolsPixmap;
Pixmap charsPixmap;
Pixmap numbersPixmap;
Pixmap buttonPixmap;
Pixmap outPixmap;
GC tickerGC;
XFontStruct *tickerFS = NULL;
name_t *names = NULL;
name_t *curTickerName = NULL;

static DAProgramOption options[] = {
    {"-display", NULL, "display to use", DOString, False, {&config.display}},
    {"-c", "--command", "cmd to run on btn-click (\"xterm -e mail\" is default)",
     DOString, False, {&config.runCmd} },
    {"-i", "--intervall",
     "number of secs between mail-status updates (1 is default)", DONatural,
     False, {&config.checkInterval} },
    {"-f", "--familyname", "tickers the family-name if available", DONone,
     False, {NULL} },
    {"-fps", "--frames", "ticker frames per second", DONatural,
     False, {&config.fps} },
    {"-s", "--shortname", "tickers the nickname (all before the '@')", DONone,
     False, {NULL} },
    {"-sc", "--symbolcolor", "symbol color-name",
     DOString, False, {&config.symbolColor} },
    {"-fc", "--fontcolor", "ticker-font color-name",
     DOString, False, {&config.fontColor} },
    {"-bc", "--backcolor", "backlight color-name",
     DOString, False, {&config.backColor} },
    {"-oc", "--offcolor", "off-light color-name",
     DOString, False, {&config.offlightColor} },
    {"-bg", "--background", "frame-background for non-shaped window",
     DOString, False, {&config.backgroundColor} },
    {"-ns", "--noshape", "make the dockapp non-shaped (combine with -w)",
     DONone, False, {NULL} },
    {"-n", "--new", "forces wmail to show new mail exclusively", DONone, False, {NULL} },
    {"-mb", "--mailbox", "specify another mailbox ($MAIL is default)", DOString, False, {&config.mailBox} },
    {"-e", "--execute", "command to execute when receiving a new mail", DOString, False, {&config.cmdOnMail} },
    {"-sf", "--statusfield", "consider the status-field of the mail header to distinguish unread mails", DONone, False, {NULL} },
	{"-rs", "--readstatus", "status field content that your client uses to mark read mails", DOString, False, {&config.readStatus} },
	{"-fn", "--tickerfont", "use specified X11 font to draw the ticker", DOString, False, {&config.useX11Font} }
};


///////////////////////////////////////////////////////////////////////////////
// prototypes

void PreparePixmaps( bool freeThemFirst );
void TimerHandler( int dummy );
void CheckMBox();
void CheckMaildir();
int TraverseDirectory( const char *name, bool isNewMail );
name_t *GetMail( unsigned long checksum );
void UpdatePixmap( bool flashMailSymbol );
void ParseMBoxFile( struct stat *fileStat );
void ParseMaildirFile( const char *fileName, unsigned long checksum,
					   struct stat *fileStat, bool isNewMail );
char *ParseFromField( char *buf );
bool SkipSender( char *address );
void InsertName( char *name, unsigned long checksum, flag_t flag );
void RemoveLastName();
void ClearAllNames();
void DrawTickerX11Font();
void DrawTickerBuildinFont();
void ButtonPressed( int button, int state, int x, int y );
void ButtonReleased( int button, int state, int x, int y );
char *XpmColorLine( const char *colorName, char *colorLine, bool disposeLine );
void ReadChecksumFile();
void WriteChecksumFile( bool writeAll );
void UpdateChecksum( unsigned long *checksum, const char *buf );
void RemoveChecksumFile();
void SetMailFlags( flag_t flag );
void MarkName( unsigned long checksum );
void DetermineState();
void UpdateConfiguration();
void CleanupNames();
char *FileNameConcat( const char *path, const char *fileName );
bool HasTickerWork();


///////////////////////////////////////////////////////////////////////////////
// implementation


void SetTimer()
{
    struct itimerval timerVal;

    timerVal.it_interval.tv_sec = 0;
    timerVal.it_interval.tv_usec = 1000000/config.fps;
    timerVal.it_value.tv_sec = 0;
    timerVal.it_value.tv_usec = 1000000/config.fps;

    setitimer( ITIMER_REAL, &timerVal, NULL );
}

int main( int argc, char **argv )
{
    char *usersHome;
    struct sigaction sa;
    int ret;
	struct stat fileStat;
	XTextProperty windowName;
	char *name = argv[0];
    DACallbacks callbacks = { NULL, &ButtonPressed, &ButtonReleased,
							  NULL, NULL, NULL, NULL };

    // read the config file and overide the default-settings
    ReadConfigFile( false );

    if( config.checksumFileName == NULL ) {
		if(( usersHome = getenv( "HOME" )) == NULL ) {
			WARNING( "HOME environment-variable is not set, placing %s in current directory!\n", WMAIL_CHECKSUM_FILE );
			config.checksumFileName = WMAIL_CHECKSUM_FILE;
		} else 
			config.checksumFileName = MakePathName( usersHome, WMAIL_CHECKSUM_FILE );
    }

    TRACE( "using checksum-file \"%s\"\n", config.checksumFileName );

    // parse cmdline-args and overide defaults and cfg-file settings
    DAParseArguments( argc, argv, options,
					  sizeof(options) / sizeof(DAProgramOption),
					  WMAIL_NAME, WMAIL_VERSION );

	if( options[0].used )
		config.givenOptions |= CL_DISPLAY;
	if( options[1].used )
		config.givenOptions |= CL_RUNCMD;
	if( options[2].used )
		config.givenOptions |= CL_CHECKINTERVAL;
	if( options[3].used ) {
		config.givenOptions |= CL_TICKERMODE;
		config.tickerMode = TICKER_FAMILYNAME;
	}
	if( options[4].used )
		config.givenOptions |= CL_FPS;
	if( options[5].used ) {
		config.givenOptions |= CL_TICKERMODE;
		config.tickerMode = TICKER_NICKNAME;
	}
	if( options[6].used )
		config.givenOptions |= CL_SYMBOLCOLOR;
	if( options[7].used )
		config.givenOptions |= CL_FONTCOLOR;
	if( options[8].used )
		config.givenOptions |= CL_BACKCOLOR;
	if( options[9].used )
		config.givenOptions |= CL_OFFLIGHTCOLOR;
	if( options[10].used )
		config.givenOptions |= CL_BACKGROUNDCOLOR;
	if( options[11].used ) {
		config.givenOptions |= CL_NOSHAPE;
		config.noshape = true;
	}
	if( options[12].used ) {
		config.givenOptions |= CL_NEWMAILONLY;
		config.newMailsOnly = true;
	}
	if( options[13].used )
		config.givenOptions |= CL_MAILBOX;
	if( options[14].used )
		config.givenOptions |= CL_CMDONMAIL;
    if( options[15].used ) {
		config.givenOptions |= CL_CONSIDERSTATUSFIELD;
		config.considerStatusField = true;
	}
	if( options[16].used )
		config.givenOptions |= CL_READSTATUS;
	if( options[17].used )
		config.givenOptions |= CL_USEX11FONT;

    if( config.mailBox == NULL )
		ABORT( "no mailbox specified - please define at least your $MAIL environment-variable!\n" );
	else if( stat( config.mailBox, &fileStat ) == 0 )
		isMaildir = S_ISDIR( fileStat.st_mode ) != 0;

	TRACE( "mailbox is of type %s\n", isMaildir ? "maildir" : "mbox" );

    // dockapp size hard wired - sorry...
    DAInitialize( config.display, "wmail", 64, 64, argc, argv );

	outPixmap = DAMakePixmap();
    PreparePixmaps( false );

    DASetCallbacks( &callbacks );
    DASetTimeout( -1 );

    sa.sa_handler = TimerHandler;
    sigemptyset( &sa.sa_mask );
    sa.sa_flags = SA_RESTART;
    ret = sigaction( SIGALRM, &sa, 0 );

    if( ret ) {
        perror( "wmail error: sigaction" );
        exit( 1 );
    }

	XStringListToTextProperty( &name, 1, &windowName );
	XSetWMName( DADisplay, DAWindow, &windowName );
    
    UpdatePixmap( false );
    DAShow();
    SetTimer();

    DAEventLoop();

    return 0;
}

void PreparePixmaps( bool freeMem )
{
    // simple recoloring of the raw xpms befor creating Pixmaps of them
    // this works as long as you don't "touch" the images...

    unsigned dummy;
	XGCValues values;

    if( config.symbolColor != NULL ) { // symbol color ?
		symbols_xpm[2] = XpmColorLine( config.symbolColor, symbols_xpm[2],
									   freeMem && ( config.colorsUsed & SYM_COLOR ));
		config.colorsUsed |= SYM_COLOR;
    } else {
		symbols_xpm[2] = XpmColorLine( "#20B2AA", symbols_xpm[2],
									   freeMem && ( config.colorsUsed & SYM_COLOR ));
		config.colorsUsed |= SYM_COLOR;
	}

    if( config.fontColor != NULL ) { // font color ?
		chars_xpm[3] = XpmColorLine( config.fontColor, chars_xpm[3],
									 freeMem && ( config.colorsUsed & FNT_COLOR ));
		numbers_xpm[3] = XpmColorLine( config.fontColor, numbers_xpm[3],
									   freeMem && ( config.colorsUsed & FNT_COLOR ));
		config.colorsUsed |= FNT_COLOR;
    } else {
		chars_xpm[3] = XpmColorLine( "#D3D3D3", chars_xpm[3],
									 freeMem && ( config.colorsUsed & FNT_COLOR ));
		numbers_xpm[3] = XpmColorLine( "#D3D3D3", numbers_xpm[3],
									   freeMem && ( config.colorsUsed & FNT_COLOR ));
		config.colorsUsed |= FNT_COLOR;
	}

    if( config.backColor != NULL ) { // backlight color ?
		main_xpm[3] = XpmColorLine( config.backColor, main_xpm[3],
									freeMem && ( config.colorsUsed & BCK_COLOR ));
		symbols_xpm[3] = XpmColorLine( config.backColor, symbols_xpm[3],
									   freeMem && ( config.colorsUsed & BCK_COLOR ));
		chars_xpm[2] = XpmColorLine( config.backColor, chars_xpm[2],
									 freeMem && ( config.colorsUsed & BCK_COLOR ));
		numbers_xpm[2] = XpmColorLine( config.backColor, numbers_xpm[2],
									   freeMem && ( config.colorsUsed & BCK_COLOR ));
		config.colorsUsed |= BCK_COLOR;
    } else {
		main_xpm[3] = XpmColorLine( "#282828", main_xpm[3],
									freeMem && ( config.colorsUsed & BCK_COLOR ));
		symbols_xpm[3] = XpmColorLine( "#282828", symbols_xpm[3],
									   freeMem && ( config.colorsUsed & BCK_COLOR ));
		chars_xpm[2] = XpmColorLine( "#282828", chars_xpm[2],
									 freeMem && ( config.colorsUsed & BCK_COLOR ));
		numbers_xpm[2] = XpmColorLine( "#282828", numbers_xpm[2],
									   freeMem && ( config.colorsUsed & BCK_COLOR ));
		config.colorsUsed |= BCK_COLOR;
	}

    if( config.offlightColor != NULL ) { // off-light color ?
		main_xpm[2] = XpmColorLine( config.offlightColor, main_xpm[2],
									freeMem && ( config.colorsUsed & OFF_COLOR ));
		numbers_xpm[4] = XpmColorLine( config.offlightColor, numbers_xpm[4],
									   freeMem && ( config.colorsUsed & OFF_COLOR ));
		config.colorsUsed |= OFF_COLOR;
    } else {
		main_xpm[2] = XpmColorLine( "#000000", main_xpm[2],
									freeMem && ( config.colorsUsed & OFF_COLOR ));
		numbers_xpm[4] = XpmColorLine( "#000000", numbers_xpm[4],
									   freeMem && ( config.colorsUsed & OFF_COLOR ));
		config.colorsUsed |= OFF_COLOR;
	}

    if( config.backgroundColor != NULL ) { // window-frame background (only seen if nonshaped) ?
		main_xpm[1] = XpmColorLine( config.backgroundColor, main_xpm[1],
									freeMem && ( config.colorsUsed & BGR_COLOR ));
		config.colorsUsed |= BGR_COLOR;
    }

    if( freeMem )
	{
		XFreePixmap( DADisplay, mainPixmap );
		XFreePixmap( DADisplay, mainPixmap_mask );
		XFreePixmap( DADisplay, symbolsPixmap );
		XFreePixmap( DADisplay, charsPixmap );
		XFreePixmap( DADisplay, numbersPixmap );
		XFreePixmap( DADisplay, buttonPixmap );

		if( tickerGC != NULL )
		{
			XFreeGC( DADisplay, tickerGC );
			tickerGC = NULL;
			if( tickerFS != NULL ) {
				XFreeFont( DADisplay, tickerFS );
				tickerFS = NULL;
			}
		}
    }

    DAMakePixmapFromData( main_xpm, &mainPixmap, &mainPixmap_mask, &dummy,  &dummy );
    DAMakePixmapFromData( symbols_xpm, &symbolsPixmap, NULL, &dummy, &dummy );
    DAMakePixmapFromData( chars_xpm, &charsPixmap, NULL, &dummy, &dummy );
    DAMakePixmapFromData( numbers_xpm, &numbersPixmap, NULL, &dummy, &dummy );
    DAMakePixmapFromData( button_xpm, &buttonPixmap, NULL, &dummy, &dummy );
  
	if( config.useX11Font != NULL )
	{
		XRectangle clipRect;

		if( config.fontColor != NULL )
			values.foreground = DAGetColor( config.fontColor );
		else
			values.foreground = DAGetColor( "#D3D3D3" );

		tickerFS = XLoadQueryFont( DADisplay, config.useX11Font );
		if( tickerFS == NULL )
			ABORT( "Cannot load font \"%s\"", config.useX11Font );

		values.font = tickerFS->fid;
		tickerGC = XCreateGC( DADisplay, DAWindow, GCForeground | GCFont,
							  &values );
		clipRect.x = 6;
		clipRect.y = 19;
		clipRect.width = 52;
		clipRect.height = 23;

		XSetClipRectangles( DADisplay, tickerGC, 0, 0, &clipRect, 1, Unsorted );
	}
  
    if( config.noshape ) // non-shaped dockapp ?
		DASetShape( None );
    else
		DASetShape( mainPixmap_mask );
}

void MarkName( unsigned long checksum )
{
    name_t *name;
    
    for( name = names; name != NULL; name = name->next ) {
		if( name->checksum == checksum ) {
			name->flag |= FLAG_READ;
			if( config.newMailsOnly )
				numMails--;
			break;
		}
    }
}

void DetermineState()
{
    name_t *name;
    
    for( name = names; name != NULL; name = name->next )
		if(!( name->flag & FLAG_READ )) {
			state = STATE_NEWMAIL;

			if( config.cmdOnMail != NULL ) {
				int ret = system( config.cmdOnMail );
	
				if( ret == 127 || ret == -1 )
					WARNING( "execution of command \"%s\" failed.\n", config.cmdOnMail );
			}
		
			break;
		}
}

void ReadChecksumFile()
{
    FILE *f = fopen( config.checksumFileName, "rb" );
    if( f != NULL ) while( !feof( f )) {
		unsigned long checksum;
		if( fread( &checksum, sizeof(long), 1, f ) != 1 )
			continue;

		MarkName( checksum );
    } else
		return;
    
    fclose( f );
}

void WriteChecksumFile( bool writeAll )
{
    FILE *f;
    TRACE( "writing checksums:" );

    if(( f = fopen( config.checksumFileName, "wb" )) != NULL ) {
		name_t *name;
		for( name = names; name != NULL; name = name->next ) {
			if( writeAll || (name->flag & FLAG_READ)) {
				fwrite( &name->checksum, sizeof(long), 1, f );
				TRACE( " %X", name->checksum );
			}
		}
    } else
		return;

    TRACE( "\n" );
    
    fclose( f );    
}

void UpdateChecksum( unsigned long *checksum, const char *buf )
{
    if( buf != NULL ) {
		unsigned int i, len = strlen( buf );
	
		for( i = 0; i < len; ++i )
			*checksum += buf[i] << (( i % sizeof(long) ) * 8 );
    }
}

void RemoveChecksumFile()
{
    TRACE( "removing checksum-file\n" );
    remove( config.checksumFileName );
}

// dummy needed because this func is a signal-handler
void TimerHandler( int dummy )
{
    static int checkMail = 0;

    if( readConfigFile ) {
		readConfigFile = false;
		UpdateConfiguration();
		checkMail = 0;
		forceRead = true;
    }

    if( checkMail == 0 )
	{
		TRACE( "checking for new mail...\n" );

		if( isMaildir )
			CheckMaildir();
		else
			CheckMBox();
	}
    
    UpdatePixmap( checkMail % config.fps < config.fps/2 );

    if( ++checkMail >= config.fps * config.checkInterval )
		checkMail = 0;
}

void CheckMBox()
{
	struct stat fileStat;

	// error retrieving file-stats -> no/zero-size file and no new/read mails
	// available
	if( stat( config.mailBox, &fileStat ) == -1 || fileStat.st_size == 0 ) {
		if( state != STATE_NOMAIL ) {
			state = STATE_NOMAIL;
			ClearAllNames();
			RemoveChecksumFile();
			forceRedraw = true;
		}
	} else {
		// file has changed -> new mails arrived or some mails removed
		if( lastModifySeconds != fileStat.st_mtime || forceRead ) {
			forceRead = false;
			ParseMBoxFile( &fileStat );
			stat( config.mailBox, &fileStat );
			forceRedraw = true;
			// file has accessed (read) -> mark all mails as "read", because
			// it cannot be decided which mails the user has read...
		} else if( lastAccessSeconds != fileStat.st_atime ) {
			state = STATE_READMAIL;
			WriteChecksumFile( true );
			if( config.newMailsOnly ) {
				numMails = 0;
				SetMailFlags( FLAG_READ );
			}
			forceRedraw = true;
		}

		lastModifySeconds = fileStat.st_mtime;
		lastAccessSeconds = fileStat.st_atime;
	}
}

void CheckMaildir()
{
	DIR *dir = NULL;
	int lastState = state;
	int lastMailCount = numMails;

	if( forceRead ) {
		forceRead = false;
		ClearAllNames();
		TRACE( "all names cleared\n" );
	}

	state = STATE_NOMAIL;

	if(( dir = opendir( config.mailBox )) != NULL )
	{
		struct dirent *dirEnt = NULL;
		//bool writeChecksums = false;
		name_t *name;

		for( name = names; name != NULL; name = name->next )
			name->visited = false;

		while(( dirEnt = readdir( dir )) != NULL )
		{
			char *fullName = FileNameConcat( config.mailBox, dirEnt->d_name );
			struct stat fileStat;

			if( !stat( fullName, &fileStat ) == 0 ) {
				WARNING( "Can't stat file/path \"%s\"\n", fullName );
				free( fullName );
				continue;
			}

			if(	S_ISDIR( fileStat.st_mode ))
			{
				if( strcmp( dirEnt->d_name, "new" ) == 0 ) {
					if( TraverseDirectory( fullName, true ) > 0 )
						state = STATE_NEWMAIL;
				}
				else if( strcmp( dirEnt->d_name, "cur" ) == 0 ) {
					if( TraverseDirectory( fullName, false ) > 0 )
						if( state != STATE_NEWMAIL )
							state = STATE_READMAIL;
				}
				// directories ".", ".." and "tmp" discarded
			}
		}
		closedir( dir );
		CleanupNames();
	} else
		WARNING( "can't open directory \"%s\"\n", config.mailBox );

	if( lastState != state || lastMailCount != numMails )
		forceRedraw = true;
}

int TraverseDirectory( const char *name, bool isNewMail )
{
	DIR *dir = NULL;
	int mails = 0;

	if(( dir = opendir( name )) != NULL )
	{
		struct dirent *dirEnt = NULL;

		while(( dirEnt = readdir( dir )) != NULL )
		{
			char *fullName = FileNameConcat( name, dirEnt->d_name );
			struct stat fileStat;
			unsigned long checksum = 0;
			name_t *name;

			if( !stat( fullName, &fileStat ) == 0 ) {
				WARNING( "Can't stat file/path \"%s\"\n", fullName );
				free( fullName );
				continue;
			}

			if(	!S_ISDIR( fileStat.st_mode ))
			{
				TRACE( "found email-file \"%s\"\n", fullName );
				UpdateChecksum( &checksum, dirEnt->d_name );

				if(( name = GetMail( checksum )) == NULL )
				{
					TRACE( "-> new file - parsing it\n" );
					ParseMaildirFile( fullName, checksum, &fileStat, isNewMail );
				}
				else {
					name->flag = isNewMail ? FLAG_INITIAL : FLAG_READ;
					name->visited = true;
				}
				++mails;
			}
		}
	}

	closedir( dir );

	return mails;
}

char *FileNameConcat( const char *path, const char *fileName )
{
	int len1 = strlen( path );
	int len2 = strlen( fileName );
	char *buf;

	if( path[len1-1] == '/' )
		--len1;

	buf = (char *)malloc( len1 + len2 + 2 );

	memcpy( buf, path, len1 );
	buf[len1] = '/';
	memcpy( &buf[len1+1], fileName, len2 );
	buf[len1+len2+1] = '\0';

	return buf;
}

name_t *GetMail( unsigned long checksum )
{
	name_t *name;
    
    for( name = names; name != NULL; name = name->next )
		if( name->checksum == checksum )
			return name;

    return NULL;
}

void UpdatePixmap( bool flashMailSymbol )
{
    int drawCount, i;

	if( !forceRedraw && !HasTickerWork() )
		return;

	forceRedraw = false;

    XCopyArea( DADisplay, mainPixmap, outPixmap, DAGC,
			   0, 0, 64, 64, 0, 0 );
    
    if( numMails > 999 )
	{
		XCopyArea( DADisplay, numbersPixmap, outPixmap, DAGC,
				   50, 0, 5, 9, 6, 49 );
		drawCount = 999;
    } else
		drawCount = numMails;

    for( i = 0; i < 3; ++i, drawCount /= 10 )
	{
		XCopyArea( DADisplay, numbersPixmap, outPixmap, DAGC,
				   (drawCount%10)*5, 0, 5, 9, 24-i*6, 49 );
		if( drawCount <= 9 )
			break;
    }

    if( buttonPressed )
	{
		XCopyArea( DADisplay, buttonPixmap, outPixmap, DAGC,
				   0, 0, 23, 11, 36, 48 );
	}

    switch( state ) {
    case STATE_NEWMAIL:
		if( flashMailSymbol )
			XCopyArea( DADisplay, symbolsPixmap, outPixmap, DAGC,
					   13, 0, 37, 12, 20, 7 );
    case STATE_READMAIL:
		XCopyArea( DADisplay, symbolsPixmap, outPixmap, DAGC,
				   0, 0, 13, 12, 7, 7 );

		if( config.useX11Font == NULL )
			DrawTickerBuildinFont();
		else
			DrawTickerX11Font();
    default: // make compiler happy
		;
    }

    DASetPixmap( outPixmap );
}

void ParseMBoxFile( struct stat *fileStat )
{
    char buf[1024];
    struct utimbuf timeStruct;
    int fromFound = 0;
    FILE *f = fopen( config.mailBox, "rt" );
    unsigned long checksum;

    state = STATE_READMAIL;
    ClearAllNames();

    numMails = 0;

    if( f == NULL ) {
		WARNING( "can't open mbox \"%s\"\n", config.mailBox );
		return;
    }

    while( fgets( buf, 1024, f ) != NULL )
	{
		if( strncmp( buf, "From ", 5 ) == 0 ) {
			fromFound = 1;
			checksum = 0;
			continue;
		}

		if( fromFound )
			UpdateChecksum( &checksum, buf );

		if( fromFound && strncasecmp( buf, "from: ", 6 ) == 0 )
		{
			if( SkipSender( buf+6 ))
				goto NEXTMAIL;

			InsertName( ParseFromField( buf+6 ), checksum, FLAG_INITIAL );

			++numMails;
			fromFound = 0;
			checksum = 0;
		} else if( config.considerStatusField && strncasecmp( buf, "status: ", 8 ) == 0 &&
				   strstr( buf+8, config.readStatus ) == NULL )
		{
			RemoveLastName();
			--numMails;
		}
    NEXTMAIL:
		;
    }

    fclose( f );
    ReadChecksumFile();

    DetermineState();

    timeStruct.actime = fileStat->st_atime;
    timeStruct.modtime = fileStat->st_mtime;
    utime( config.mailBox, &timeStruct );
}

void ParseMaildirFile( const char *fileName, unsigned long checksum,
					   struct stat *fileStat, bool isNewMail )
{
	char buf[1024];
	struct utimbuf timeStruct;
	FILE *f = fopen( fileName, "rt" );

	if( f == NULL )
	{
		WARNING( "can't open maildir file \"%s\"\n", fileName );
		return;
	}

	while( fgets( buf, 1024, f ) != NULL )
	{
		if( strncasecmp( buf, "from: ", 6 ) == 0 )
		{
			if( SkipSender( buf+6 ))
				break;

			InsertName( ParseFromField( buf+6 ), checksum,
						isNewMail ? FLAG_INITIAL : FLAG_READ );

			//++numMails;
		}
	}

	fclose( f );

	timeStruct.actime = fileStat->st_atime;
    timeStruct.modtime = fileStat->st_mtime;
    utime( fileName, &timeStruct );	
}

char *ParseFromField( char *buf )
{
	parse_state_t state = STATE_FULLNAME;
	int fullNameEncoded = 0;
	int saveAtCharPos = -1;
	char *fullName;
	char *addressName;
	char *atChar = NULL;
	char *c;
	int maxLen = strlen( buf ) + 1;
	char *comment;

	// FIXME: Uhm, those mallocs might fail...

	fullName = malloc( maxLen );
	addressName = malloc( maxLen );
	comment = malloc( maxLen );

	memset( fullName, '\0', maxLen );
	memset( addressName, '\0', maxLen );
	memset( comment, '\0', maxLen );

	// FIXME: Don't do that "encoded" dance.  It's not intended by
	// RFC2047, and it's better to just do it in the end.
	// Cleaner.

	for( c = buf; *c != '\0'; ++c )
	{
		switch( state ) {
		case STATE_FULLNAME:

			switch( *c ) {
			case '"':
				state = STATE_QUOTED_FULLNAME;
				continue;
			case '<':
				if( fullName[0] != '\0' && 
					fullName[ strlen( fullName ) - 1 ] == ' ' )
					fullName[ strlen( fullName ) - 1 ] = '\0';
				state = STATE_ADDRESS;
				continue;
			case '@':
				saveAtCharPos = strlen( fullName );
				fullName[ saveAtCharPos ] = *c;
				continue;
			case '(':
				state = STATE_COMMENT;
				continue;
			case '=':
				if( *(c+1) == '?' ) {
					++c;
					fullNameEncoded = 1;
					state = STATE_ENCODED_FULLNAME;
					continue;
				} // else do the default action
			default:
				fullName[ strlen( fullName ) ] = *c;
			}
			continue;
			
		case STATE_QUOTED_FULLNAME:
		    
			switch( *c ) {
			case '\\':
				fullName[ strlen( fullName ) ] = *(++c);
				continue;
			case '"':
				state = STATE_FULLNAME;
				continue;
			default:
				fullName[ strlen( fullName ) ] = *c;
			}
			continue;

		case STATE_ENCODED_FULLNAME:

			switch( *c ) {
			case '?':
				if( *(c+1) == '=' ) {
					++c;
					state = STATE_FULLNAME;
					continue;
				}
			default:
				; // do nothing... COMING SOON: decode at least latin1
			}
			continue;

		case STATE_ADDRESS:

			switch( *c ) {
			case '"':
				state = STATE_QUOTED_ADDRESS;
			case '>':
			case '\n':
				// FIXME: Shouldn't it break here?
				// Since the address is finished?
				continue;
			case '@':
				atChar = &addressName[ strlen( addressName ) ];
				*atChar = *c;
				continue;
			default:
				addressName[ strlen( addressName ) ] = *c;
			}
			continue;

		case STATE_QUOTED_ADDRESS:

			switch( *c ) {
			case '"':
				state = STATE_ADDRESS;
				continue;
			case '\\':
				addressName[ strlen( addressName ) ] = *(++c);
				continue;
			default:
				addressName[ strlen( addressName ) ] = *c;
			}
			continue;
			case STATE_COMMENT:
				switch( *c ) {
				case ')':
					state = STATE_FULLNAME;
					continue;
				default:
					comment[ strlen( comment ) ] = *c;
					continue;
				}
				continue;
		}
	}

	if (*comment) {
		//WARNING("Comment seen: %s\nIn: %s\nFullname: %s\nAddress: %s\n", comment, buf, fullName, addressName);
		// Comment seen: if there's an address, append to
		// fullname.  If no address, copy fullname to address
		// and comment to fullname.
		if (*addressName) {
			strcat(fullName, "(");
			strcat(fullName, comment);
			strcat(fullName, ")");
		} else {
			strcpy(addressName, fullName);
			strcpy(fullName, comment);
		}
	}

	//WARNING("Fullname: %s\nAddress: %s\n", fullName, addressName);

	// what name should be tickered
	if( config.tickerMode == TICKER_FAMILYNAME && fullName[0] != '\0' && !fullNameEncoded ) {
		free( addressName );
		return fullName;
	} else {
		if( state == STATE_FULLNAME ) {
			strcpy( addressName, fullName );
			if( saveAtCharPos != -1 )
				atChar = &addressName[saveAtCharPos];
		}
		if( config.tickerMode == TICKER_NICKNAME ) {
			if( atChar != NULL )
				*atChar = '\0';
		}
		free( fullName );
		return addressName;
	}
}

bool SkipSender( char *address )
{
	char **skipName;
	int len = strlen( address );

	// remove trailing '\n' got from fgets
	if( address[len-1] == '\n' )
		address[len-1] = '\0';

	for( skipName = config.skipNames;
		 skipName != NULL && *skipName != NULL; skipName++ )
	{
		TRACE( "comparing \"%s\" and \"%s\"\n", *skipName, address );
			
		// call libc-fnmatch (wildcard-match :-) !
		if( !fnmatch( *skipName, address, 0 )) {
			TRACE( "skipping sender \"%s\"\n", *skipName );
			return true;
		}
	}

	return false;
}

void InsertName( char *name, unsigned long checksum, flag_t flag )
{
    name_t *item;

    TRACE( "insertName: %X, \"%s\"\n", checksum, name );
    item = (name_t *)malloc( sizeof( name_t ));
    item->name = name; /*strdup( name );*/
    item->checksum = checksum;
    item->flag = flag;
	item->visited = true;
    item->next = names;
    names = item;

    namesChanged = true;
}

void RemoveLastName()
{
    if( names != NULL ) {
		name_t *name = names;
		names = names->next;
		free( name );
    }
}

void ClearAllNames()
{
    name_t *name, *nextName;
    
    for( name = names; name != NULL; name = nextName ) {
		nextName = name->next;

		free( name->name );
		free( name );
    }

    names = NULL;
    numMails = 0;

    namesChanged = true;
}

void SetMailFlags( flag_t flag )
{
    name_t *name;
    
    for( name = names; name != NULL; name = name->next )
		name->flag |= flag;
}

void DrawTickerX11Font()
{
	// 49x21+7+20 out-drawable size

	static int insertAt;

	if( curTickerName == NULL || namesChanged )
	{
		for( curTickerName = names;
			 curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ );
			 curTickerName = curTickerName->next );

		if( curTickerName == NULL )
			return;

		namesChanged = false;
		insertAt = 54;
    }

	XDrawString( DADisplay, outPixmap, tickerGC, insertAt,
				 41-tickerFS->max_bounds.descent,
				 curTickerName->name, strlen( curTickerName->name ));

	--insertAt;

	if( insertAt < -XTextWidth( tickerFS, curTickerName->name,
								  strlen( curTickerName->name )) + 6 )
	{
		do {
			curTickerName = curTickerName->next;
		} while( curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ ));

		if( curTickerName != NULL ) {
			insertAt = 54;
		}
	}
}

void DrawTickerBuildinFont()
{
    // 49x21+7+20 out-drawable size
    // 14x21 font-character size

    static int insertAt;
    static int takeItFrom;

    int leftSpace;
    int drawTo;
    unsigned char *currentChar;
    
    if( names == NULL )
		return;

    if( curTickerName == NULL || namesChanged ) {

		for( curTickerName = names;
			 curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ );
			 curTickerName = curTickerName->next );

		if( curTickerName == NULL )
			return;

		insertAt = 57;
		takeItFrom = 0;
		namesChanged = false;
    }

    leftSpace = takeItFrom % 14;

    for( currentChar = (unsigned char *)&curTickerName->name[takeItFrom/14],
			 drawTo = insertAt; *currentChar != '\0'; ++currentChar )
    {

		int outChar = (*currentChar < 32 || *currentChar >= 128) ? '?' :
			*currentChar;
		int charWidth = 57-drawTo >= 14 ? 14 - leftSpace : 57-drawTo;
	
		XCopyArea( DADisplay, charsPixmap, outPixmap, DAGC,
				   (outChar-32)*14+leftSpace, 0, charWidth, 21, drawTo, 20 );

        leftSpace = 0;
        drawTo += charWidth;
        
        if( drawTo > 57 )
            break;
    }

    if( --insertAt < 7 ) {
		insertAt = 7;
		takeItFrom++;

		if( takeItFrom/14 >= strlen( curTickerName->name )) {

			do {
				curTickerName = curTickerName->next;
			} while( curTickerName != NULL && config.newMailsOnly && ( curTickerName->flag & FLAG_READ ));

			if( curTickerName != NULL ) {
				takeItFrom = 0;
				insertAt = 57;
			}
		}
    }
}
	
void ButtonPressed( int button, int state, int x, int y )
{
    if( x >= 35 && x <= 59 && y >= 47 && y <= 59 ) {
		buttonPressed = true;
		forceRedraw = true;
    } else
		// reread the config file
		readConfigFile = true;
}

void ButtonReleased( int button, int state, int x, int y )
{
    buttonPressed = false;
	forceRedraw = true;

    if( x >= 35 && x <= 59 && y >= 47 && y <= 59 ) {
		int ret = system( config.runCmd );
	
		if( ret == 127 || ret == -1 )
			WARNING( "execution of command \"%s\" failed.\n", config.runCmd );
    }
}

void GetHexColorString( const char *colorName, char *xpmLine )
{
    XColor color;

    if( XParseColor( DADisplay,
					 DefaultColormap( DADisplay, DefaultScreen( DADisplay )),
					 colorName, &color ))
    {
		sprintf( xpmLine, "%02X%02X%02X", color.red>>8, color.green>>8,
				  color.blue>>8 );
    } else
		WARNING( "unknown colorname: \"%s\"\n", colorName );
}

char *XpmColorLine( const char *colorName, char *colorLine, bool disposeLine )
{
    char *newLine = strdup( colorLine );
    char *from = strrchr( newLine, '#' );
    
    if( from == NULL && !strcasecmp( &colorLine[ strlen( colorLine ) - 4 ], "none" ))  {
		// if no # found, it should be a None-color line
		free( newLine );
		newLine = malloc( 12 );
		strcpy( newLine, " \tc #" );
		newLine[11] = '\0';
		from = newLine + 4;
    }

    if( disposeLine )
		free( colorLine );
    
    GetHexColorString( colorName, from+1 );
    
    return newLine;
}

void UpdateConfiguration()
{
	struct stat fileStat;

    TRACE( "reading configuration file...\n" );

    ReadConfigFile( true );

	// if no path/name to an mbox or maildir inbox directory was given,
	// use the environment
    if( config.mailBox == NULL )
		config.mailBox = getenv( "MAIL" );

	// mbox or maildir ?
	if( config.mailBox != NULL && stat( config.mailBox, &fileStat ) == 0 )
		isMaildir = S_ISDIR( fileStat.st_mode ) != 0;
	else
		isMaildir = false;

	TRACE( "mailbox is of type %s\n", isMaildir ? "maildir" : "mbox" );

    PreparePixmaps( true );

    SetTimer();
}

void CleanupNames()
{
	name_t *name, *last = NULL, *nextName;

	numMails = 0;

	for( name = names; name != NULL; name = nextName )
	{
		nextName = name->next;

		if( !name->visited ) {
			if( last == NULL )
				names = name->next;
			else
				last->next = name->next;

			free( name );
		} else {
			last = name;

			if( !config.newMailsOnly || (name->flag & FLAG_READ) == 0 )
				++numMails;
		}
	}
}

bool HasTickerWork()
{
	name_t *nextTickerName;

	if( names == NULL )
		return false;

	if( curTickerName == NULL || namesChanged ) {

		for( nextTickerName = names;
			 nextTickerName != NULL && ( config.newMailsOnly && ( nextTickerName->flag & FLAG_READ ));
			 nextTickerName = nextTickerName->next );

		if( nextTickerName == NULL )
			return false;
	}

	return true;
}
