ViewCVS and CVS Help

[elog] / trunk / src / elogd.c
Project Root:

View of /trunk/src/elogd.c

Parent Directory | Revision Log
Revision: 1472 - (download)
Thu Aug 4 20:26:35 2005 UTC (4 years, 3 months ago) by ritt
File size: 799581 byte(s)
Do not distinguish between invalid user name and invalid password for security reasons
/********************************************************************\

   Name:         elogd.c
   Created by:   Stefan Ritt

   Contents:     Web server program for Electronic Logbook ELOG

   $Log$
   Revision 1.739  2005/08/04 20:26:17  ritt
   Do not distinguish between invalid user name and invalid password for security reasons

   Revision 1.738  2005/08/04 20:06:23  ritt
   Added error output if password file cannot be written

   Revision 1.737  2005/08/04 18:28:48  ritt
   Implemented 'Email Format = 64' for only attachment names

   Revision 1.736  2005/08/03 20:43:28  ritt
   Improved smileys

   Revision 1.735  2005/08/03 20:30:19  ritt
   Implemented 'resolve host names'

   Revision 1.734  2005/08/02 18:39:12  ritt
   Remove obsolete options

   Revision 1.733  2005/07/29 14:35:11  ritt
   Added icons for 'show new/all'

   Revision 1.732  2005/07/29 09:06:33  ritt
   Moved 'filter menu text' left of 'show only new entries'

   Revision 1.731  2005/07/29 08:37:06  ritt
   Added 'filter menu text'

   Revision 1.730  2005/07/27 20:51:24  ritt
   Added validity check for year in date/time attributes

   Revision 1.729  2005/07/27 18:42:53  ritt
   Fixed HTML errors

   Revision 1.728  2005/07/26 19:19:01  ritt
   Use rsputs3 for quotes

   Revision 1.727  2005/07/26 18:21:27  ritt
   Added 'Edit page title'

   Revision 1.726  2005/07/26 18:15:19  ritt
   Added 'reply comment'

   Revision 1.725  2005/07/26 18:11:06  ritt
   Changed 'summary page title' to 'list page title'

   Revision 1.724  2005/07/26 09:55:43  ritt
   Changed 'rss feed' to 'alternate'

   Revision 1.723  2005/07/26 07:59:46  ritt
   Added $elogbook

   Revision 1.722  2005/07/26 07:27:00  ritt
   Version 2.6.0-beta3

   Revision 1.721  2005/07/26 07:03:41  ritt
   Set correct message_id for threaded display

   Revision 1.720  2005/07/25 20:09:38  ritt
   Made elog: links abolute for email notification

   Revision 1.719  2005/07/25 19:25:37  ritt
   Implemented 'change <attrib>' and 'list change <attrib>'

   Revision 1.718  2005/07/25 18:37:21  ritt
   Remove </ br> after [/code]

   Revision 1.717  2005/07/25 18:04:12  ritt
   Applied pointer casting patch from Recai

   Revision 1.716  2005/07/23 16:40:20  ritt
   Added even more CR's

   Revision 1.715  2005/07/23 16:30:42  ritt
   Fixed {n} display with ROptions

   Revision 1.714  2005/07/23 16:22:14  ritt
   Added condition evaluation in list display

   Revision 1.713  2005/07/23 13:35:21  ritt
   Replaced tcp_hostname by listen_interface

   Revision 1.712  2005/07/22 21:33:41  ritt
   Added line break for thread display

   Revision 1.710  2005/07/22 12:34:47  ritt
   Added <pre> to [code]

   Revision 1.709  2005/07/21 09:19:13  ritt
   Put back erraneously deleted lines

   Revision 1.708  2005/07/20 21:24:01  ritt
   Implemented conditional attributes also for display

   Revision 1.707  2005/07/20 20:27:54  ritt
   Fixed bug with non-interpreted <b> in attributes

   Revision 1.706  2005/07/20 20:13:11  ritt
   Made 'list' work together with 'start page = 0?cmd=Last'

   Revision 1.705  2005/07/20 19:37:42  ritt
   Do not interprete ELCode inside [code]...[/code]

   Revision 1.704  2005/07/20 18:53:00  ritt
   Added 'email attributes' option

   Revision 1.703  2005/07/16 10:18:17  ritt
   Added '\' as escape character for HTML and ELCode tags

   Revision 1.702  2005/07/11 10:28:04  ritt
   Added 'title' to all icons for FireFox

   Revision 1.701  2005/07/08 20:18:04  ritt
   Treat attributes with <img> as HTML

   Revision 1.700  2005/07/07 20:02:43  ritt
   Added 'suppress default = 3'

   Revision 1.699  2005/07/07 19:47:11  ritt
   Tooltips only for logbooks, not groups

   Revision 1.698  2005/07/07 18:24:13  ritt
   Added tooltip to logbook selection bar

   Revision 1.697  2005/07/05 20:49:22  ritt
   Made reply_to links absolute

   Revision 1.696  2005/07/05 20:39:16  ritt
   Fixed <br> for quotes

   Revision 1.695  2005/07/05 20:28:58  ritt
   Converted links to absolute URLs for threads

   Revision 1.694  2005/07/05 20:00:43  ritt
   Removed wrong '&'

   Revision 1.693  2005/07/05 19:06:27  ritt
   Use <tr> for AF_MULTI_LINE

   Revision 1.692  2005/07/05 09:45:41  ritt
   Fixed bug with new_entries cut

   Revision 1.691  2005/07/04 21:12:58  ritt
   Put separate table around icons

   Revision 1.690  2005/07/04 21:03:24  ritt
   Replaced more <nowrap>

   Revision 1.689  2005/07/04 20:50:41  ritt
   Replaced <nowrap>

   Revision 1.688  2005/07/04 20:43:39  ritt
   'Show all entries' keeps parameters from original search

   Revision 1.687  2005/07/04 20:17:25  ritt
   Put absolute link for CSS

   Revision 1.686  2005/06/30 05:18:43  ritt
   Added image insertion by JScript

   Revision 1.685  2005/06/24 20:22:33  ritt
   Fixed bug with conditional attributes and quick filters

   Revision 1.684  2005/06/17 20:45:40  ritt
   Changed 'find menu' to 'list menu'

   Revision 1.683  2005/06/17 20:31:41  ritt
   Fixed bug with user activation and global password files

   Revision 1.682  2005/06/16 20:36:26  ritt
   Fixed problem with reverse sort in quick filters

   Revision 1.681  2005/06/16 19:52:19  ritt
   Version 2.6.0-beta2

   Revision 1.680  2005/06/16 11:19:37  ritt
   Applied patch from Emiliano with strftime workaround

   Revision 1.679  2005/06/14 13:09:04  ritt
   Use absolute http:// links for smileys

   Revision 1.678  2005/06/13 09:03:31  ritt
   Implemented last_activity, HTML email

   Revision 1.677  2005/06/04 11:58:20  ritt
   Switched login page to POST method

   Revision 1.676  2005/06/04 10:55:08  ritt
   Only use letters in randomly generated recovery password

   Revision 1.675  2005/06/04 08:52:15  ritt
   Fixed 'pippo-bug' of 'list display' option

   Revision 1.674  2005/05/27 12:39:17  ritt
   Applied patch from Emiliano

   Revision 1.673  2005/05/27 12:29:00  ritt
   Fixed problem that encoding could not be selected on new entries

   Revision 1.672  2005/05/27 10:59:51  ritt
   Fixed bug with <br / ...text...>

   Revision 1.671  2005/05/17 21:15:35  ritt
   Hide smileys by default

   Revision 1.670  2005/05/17 21:12:36  ritt
   Use orig_author for reply quoting

   Revision 1.669  2005/05/17 21:00:53  ritt
   Put JS in separate file

   Revision 1.668  2005/05/17 13:00:47  ritt
   Fixed bug with missing 'full' view

   Revision 1.667  2005/05/17 11:28:19  ritt
   Version 2.6.0-beta

   Revision 1.666  2005/05/17 11:26:14  ritt
   Remove hard wraps for textarea in non-plain mode

   Revision 1.665  2005/05/17 11:21:16  ritt
   Added strnieq()

   Revision 1.664  2005/05/17 11:08:48  ritt
   Added IE javascript

   Revision 1.663  2005/05/17 07:02:46  ritt
   Changed 'set current time' to 'insert current time'

   Revision 1.662  2005/05/16 20:02:39  ritt
   Implemented smiley bar

   Revision 1.661  2005/05/15 12:43:30  ritt
   Implemented url and img tags

   Revision 1.660  2005/05/14 21:45:37  ritt
   Implemented first ELCode tags

   Revision 1.659  2005/05/14 11:14:10  ritt
   Version 2.5.9-4

   Revision 1.658  2005/05/12 21:13:52  ritt
   Changed xmalloc to avoid crash when running as a service

   Revision 1.657  2005/05/12 20:59:31  ritt
   Added printf() to avoid windows crash on long config files

   Revision 1.656  2005/05/12 20:02:12  ritt
   Version 2.5.9-3

   Revision 1.655  2005/05/11 14:00:08  ritt
   Added confimation dialog for removing users

   Revision 1.654  2005/05/10 11:27:23  ritt
   Supersede 'Display mode' by cookie

   Revision 1.653  2005/05/10 07:17:48  ritt
   Store list mode in cookie

   Revision 1.652  2005/05/09 11:08:36  ritt
   Fixed compiler warning

   Revision 1.651  2005/05/09 11:06:28  ritt
   Version 2.5.9-2

   Revision 1.650  2005/05/09 11:02:26  ritt
   Change '(' to '\(' in quick filters for regex matching

   Revision 1.649  2005/05/09 07:50:00  ritt
   Re-applied mod (?) to show copy/move targets in drop-down box

   Revision 1.648  2005/05/09 07:00:15  ritt
   Fixed buffer overflow for header_buffer

   Revision 1.647  2005/05/05 09:26:16  ritt
   Added error handling for invalid XML file

   Revision 1.646  2005/05/02 15:19:01  ritt
   Changed wrong sizeof(), thanks to Emiliano

   Revision 1.645  2005/05/02 15:06:55  ritt
   Removed superflous mailto:, thanks to Emiliano

   Revision 1.644  2005/05/02 12:57:19  ritt
   Remove empty attribute values in RSS feed

   Revision 1.643  2005/05/02 10:21:13  ritt
   Fixed compiler warning

   Revision 1.642  2005/05/02 10:17:09  ritt
   Put strlcpy/strlcat in separate source file

   Revision 1.641  2005/05/02 10:06:33  ritt
   Version 2.5.9-1

   Revision 1.640  2005/04/29 20:26:12  ritt
   Added str_escape from Emiliano Gabrielli

   Revision 1.639  2005/04/29 20:04:51  ritt
   Implemented 'case sensitive search' flag

   Revision 1.638  2005/04/27 10:43:43  ritt
   Applied patch from Emiliano to fix possible buffer overflow

   Revision 1.637  2005/04/22 13:42:51  ritt
   Version 2.5.8-6

   Revision 1.636  2005/04/22 13:41:18  ritt
   Fixed crashes with very long (revisions) attributes

   Revision 1.635  2005/04/21 20:50:02  ritt
   Fixed missing icons in top groups

   Revision 1.634  2005/04/21 06:58:16  ritt
   Fixed wrong year under IE with datetime attibutes

   Revision 1.633  2005/04/20 18:31:38  ritt
   Version 2.5.8-5

   Revision 1.632  2005/04/19 06:43:45  ritt
   Changed indentation

   Revision 1.631  2005/04/18 20:39:32  ritt
   Added SMTP error reporting

   Revision 1.630  2005/04/18 18:49:31  ritt
   Added 'set current time/date' funcitonality

   Revision 1.629  2005/04/15 20:25:53  ritt
   Read password files in el_index_logbooks

   Revision 1.628  2005/04/15 20:13:35  ritt
   Fixed problem with password file creation through logbook selection page

   Revision 1.627  2005/04/15 19:48:36  ritt
   Use base64 encoding for email subject

   Revision 1.626  2005/04/15 19:33:29  ritt
   Adde 'Raw' mode for export

   Revision 1.625  2005/04/14 13:41:50  ritt
   Use charset from config file in sending email

   Revision 1.624  2005/04/13 19:39:46  ritt
   Create password file if not present

   Revision 1.623  2005/04/13 08:05:05  ritt
   Fixed bug with conditions and '&'

   Revision 1.622  2005/04/12 07:11:27  ritt
   Fixed crash on 'm*' regex search

   Revision 1.621  2005/04/07 20:19:26  ritt
   Fixed compiler warning

   Revision 1.620  2005/04/07 20:17:28  ritt
   Redirect to source logbook for copy/move

   Revision 1.619  2005/04/07 19:41:38  ritt
   Open attachments in separate browser window

   Revision 1.618  2005/04/06 19:45:13  ritt
   Reread password file(s) after HUP signal

   Revision 1.617  2005/04/06 19:20:57  ritt
   Version 2.5.8-3

   Revision 1.616  2005/04/06 19:12:25  ritt
   Fixed bug with lowercase conditions

   Revision 1.615  2005/04/01 19:25:52  ritt
   Implemented drop-down boxes for copy/move also on individual entry page

   Revision 1.614  2005/03/31 20:43:45  ritt
   Added 'Duplicate' to default menu commands

   Revision 1.613  2005/03/31 20:37:05  ritt
   Implemented 'Duplicate' command

   Revision 1.612  2005/03/31 18:15:32  ritt
   Implemented seconds for datetime

   Revision 1.611  2005/03/30 08:59:35  ritt
   Implemented datetime format

   Revision 1.610  2005/03/29 21:20:37  ritt
   Fixed find form for DATETIME

   Revision 1.609  2005/03/29 21:15:24  ritt
   Implemented 'Type <attrib> = datetime'

   Revision 1.608  2005/03/29 13:31:37  ritt
   Handle logbook subscriptios correctly with self register = 3

   Revision 1.607  2005/03/29 13:16:47  ritt
   Subsittute ' ' in attachment file names by '_'

   Revision 1.606  2005/03/29 11:52:59  ritt
   Changed is_ascii() to accept umlaute

   Revision 1.605  2005/03/29 11:45:46  ritt
   Check for '.' in btou()

   Revision 1.604  2005/03/29 10:22:16  ritt
   Version 2.5.8-2

   Revision 1.603  2005/03/29 07:52:06  ritt
   Renamed it to 'expand selection page'

   Revision 1.602  2005/03/29 07:50:15  ritt
   Implemented 'expand selection', defaulting to one

   Revision 1.601  2005/03/29 07:30:30  ritt
   Applied patch from Recai to give precedence to directories from the command line

   Revision 1.600  2005/03/29 07:19:37  ritt
   Added add_special_xxx

   Revision 1.599  2005/03/27 19:57:23  ritt
   Adjusted code for mxml modifications

   Revision 1.598  2005/03/24 22:58:32  ritt
   Create root node 'list'

   Revision 1.597  2005/03/24 18:41:16  ritt
   Fixed small pointer bug

   Revision 1.596  2005/03/24 12:54:05  ritt
   Fixed problem with '&' in href links

   Revision 1.595  2005/03/24 10:37:43  ritt
   Fixed problem with 'protect selection page' ans invalid authentication

   Revision 1.594  2005/03/24 09:38:15  ritt
   Fixed bug with crashing elog on 'protect selection page'

   Revision 1.593  2005/03/24 09:22:38  ritt
   XPath starts now with index 1

   Revision 1.592  2005/03/21 16:22:31  ritt
   Fixed typo

   Revision 1.591  2005/03/21 16:16:52  ritt
   Version 2.5.8-1

   Revision 1.590  2005/03/21 07:37:43  ritt
   Fixed warnings under cygwin

   Revision 1.589  2005/03/16 21:07:43  ritt
   Fixed problem with MOptions and elog

   Revision 1.588  2005/03/14 20:53:23  ritt
   Implemented option 'link display'

   Revision 1.587  2005/03/14 20:07:46  ritt
   Show attributes as HTML if they contain '<b>' etc.

   Revision 1.586  2005/03/14 20:00:34  ritt
   Fixed problem with multiple extendable options

   Revision 1.585  2005/03/14 08:36:56  ritt
   Added thumbnail display for list display

   Revision 1.584  2005/03/08 08:51:25  ritt
   Fixed bug with resubmit box and conditional attributes

   Revision 1.583  2005/03/08 08:42:17  ritt
   Show logbook list for subscription for new user

   Revision 1.582  2005/03/08 08:29:47  ritt
   Fixed problem with self-register and password file

   Revision 1.581  2005/03/03 16:03:39  ritt
   Fixed problem with top groups and password files

   Revision 1.580  2005/03/03 15:35:00  ritt
   Removed debugging statements

   Revision 1.579  2005/03/03 15:33:05  ritt
   Keep parsed password files in memory

   Revision 1.578  2005/03/02 21:03:16  ritt
   Implemented email subscriptions for individual logbooks

   Revision 1.577  2005/03/02 17:13:24  ritt
   Implemented central load_password_file()

   Revision 1.576  2005/03/02 16:29:20  ritt
   Encode '&' correctly if present in 'Start page' option

   Revision 1.575  2005/03/02 14:20:16  ritt
   Fixed wrong comma display together with 'List display'

   Revision 1.574  2005/03/02 14:04:18  ritt
   Implemented support for thumbnail display

   Revision 1.573  2005/03/02 00:01:23  ritt
   Fixed compiler warnings

   Revision 1.572  2005/03/01 23:48:17  ritt
   Implemented MXML for password file

   Revision 1.571  2005/02/22 09:34:04  ritt
   Fixed bug with logbook names containing blanks and 'List' link

   Revision 1.570  2005/02/22 08:55:20  ritt
   Fixed bug with $attachments substitution on shell command

   Revision 1.569  2005/02/22 08:15:19  ritt
   Applied patch from Emiliano Gabrielli to use chkext()

   Revision 1.568  2005/02/22 07:19:41  ritt
   Fixed bug in parse_config_file()

   Revision 1.567  2005/02/20 20:18:26  ritt
   Fixed problem with conditional attributes and presets

   Revision 1.566  2005/02/20 19:39:19  ritt
   Improved speed by pre-parsing configuration file

   Revision 1.565  2005/02/20 14:30:07  ritt
   Applied patch from Heiko Scheit fixing problem with 'Show attributes' causing the 'Format ...' options to be ignored

   Revision 1.564  2005/02/16 14:50:40  ritt
   Implemented $attachements subsitution in 'execute' command

   Revision 1.563  2005/02/16 08:22:25  ritt
   Added web link to regular expressions

   Revision 1.562  2005/02/16 08:15:00  ritt
   Test regular expressions for validity

   Revision 1.561  2005/02/15 18:55:35  ritt
   Use 'attachmentframe' style

   Revision 1.560  2005/02/14 20:00:06  ritt
   Fixed HTML code to pass validator

   Revision 1.559  2005/02/14 19:52:10  ritt
   Broke apart long code line

   Revision 1.558  2005/02/14 10:57:11  ritt
   Fixed compiler warning

   Revision 1.557  2005/02/14 10:52:36  ritt
   Version 2.5.7-1

   Revision 1.556  2005/02/14 10:44:16  ritt
   Fixed buffer overflow in decode_post()

   Revision 1.555  2005/02/13 15:42:39  ritt
   Solved bug with 'fixed' ROptions attributes

   Revision 1.554  2005/02/12 16:14:19  ritt
   Removed superflous </a>

   Revision 1.553  2005/02/12 16:06:03  ritt
   - Added missing 'alt' tags to images
   - Use admin user email in 'from' field if nothing else is availabe
   - 'Preset attribute' now also works with conditional attributes
   - Fixed wrong '%S' instead of '%s' in charset
   - Use chkext() for checking extensions

   Revision 1.552  2005/02/11 11:59:48  ritt
   Do not display .EPS files inline as ASCII files

   Revision 1.551  2005/02/03 15:46:10  ritt
   Fixed problem with conditional attributes in French

   Revision 1.550  2005/01/31 19:51:49  ritt
   Removed superfloous 'mailto:' in substitution

   Revision 1.549  2005/01/30 16:43:21  ritt
   Added highlighting for search results in attributes

   Revision 1.548  2005/01/26 20:32:50  ritt
   Fixed other problem in searching

   Revision 1.547  2005/01/26 19:25:15  ritt
   Fixed bug yielding in wrong search results

   Revision 1.546  2005/01/25 21:06:56  ritt
   Version 2.5.6-1

   Revision 1.545  2005/01/25 20:49:18  ritt
   Implemented 'Mirror exclude'

   Revision 1.544  2005/01/25 16:31:40  ritt
   Switched from GIF to PNG

   Revision 1.543  2005/01/21 22:51:25  ritt
   Remove any CR/LF from attributes submitted from browser

   Revision 1.542  2005/01/21 22:21:24  ritt
   Fixed compiler warning

   Revision 1.541  2005/01/21 22:06:02  ritt
   Implemented OR'ing of MOptions values in find page

   Revision 1.540  2005/01/21 20:17:20  ritt
   Display current entry in thread bold

   Revision 1.539  2005/01/19 21:08:32  midas
   Display thread in single entry page if present

   Revision 1.538  2005/01/17 20:01:33  midas
   Added note about changing 'max content length'

   Revision 1.537  2005/01/17 16:14:51  midas
   Implemented 'show attributes' and removed 'hidden attributes'

   Revision 1.536  2005/01/17 15:42:05  midas
   Made extendable attributes work with MOptions

   Revision 1.535  2005/01/17 14:01:40  midas
   Added message_id and date to CSV export

   Revision 1.534  2005/01/06 10:02:34  midas
   Implemented 'hidden attributes'

   Revision 1.533  2005/01/06 08:51:22  midas
   Made extendable attributes work with conditional attributes

   Revision 1.532  2005/01/05 20:36:49  midas
   Logbook hierarchy can now be deeper than two levels

   Revision 1.531  2005/01/05 15:56:10  midas
   Cancel button on 'create new logbook' now also works without password files

   Revision 1.530  2005/01/05 09:05:49  midas
   Fixed non-functioning Cancel button in 'create new logbook'

   Revision 1.529  2005/01/04 19:42:52  midas
   Version 2.5.5-4

   Revision 1.528  2005/01/04 10:31:10  midas
   Changed date format to RFC-822 in RSS feed

   Revision 1.527  2004/12/29 13:42:38  midas
   Patch from Recai

   Revision 1.526  2004/12/20 16:14:58  midas
   Version 2.5.5-3

   Revision 1.525  2004/12/20 16:14:04  midas
   Use encoded logbook name in substitutions

   Revision 1.524  2004/12/20 12:41:53  midas
   Fixed endless loop with handcrafted POST header

   Revision 1.523  2004/12/18 16:18:58  midas
   Changed locale to 'C' for email header date

   Revision 1.522  2004/12/17 22:18:29  midas
   Use hostname from -n parameter for redirection if given

   Revision 1.521  2004/12/17 21:50:28  midas
   Fixed attribute substitutions in 'use email from'

   Revision 1.520  2004/12/17 21:32:50  midas
   Fixed problem with invalid RFC2822 date in email header for different locale

   Revision 1.519  2004/12/13 20:53:12  midas
   Fixed problem with conditional attributes and edit

   Revision 1.518  2004/12/06 20:42:51  midas
   Fixed problem with 'Back' button

   Revision 1.517  2004/12/05 11:52:53  midas
   Added date check

   Revision 1.516  2004/12/05 11:40:52  midas
   Implemented 'sort attribute'

   Revision 1.515  2004/12/04 16:18:50  midas
   Look for parameters first under conditions, then unconditional if not found

   Revision 1.514  2004/11/23 11:33:59  midas
   Fixed bug in XML export

   Revision 1.513  2004/11/17 14:39:16  midas
   Implemented USERLIST as attribute type

   Revision 1.512  2004/11/16 15:35:17  midas
   Version 2.5.5-2

   Revision 1.511  2004/11/16 15:32:03  midas
   Increased NAME_LENGTH to 1500, requires a stack size of 4MB

   Revision 1.510  2004/11/15 11:42:17  midas
   Fixed typo

   Revision 1.509  2004/11/15 09:57:33  midas
   Changed charset for RSS feeds

   Revision 1.508  2004/11/06 16:45:14  midas
   Fixed wrong link if last entry in logbook has been moved

   Revision 1.507  2004/11/03 10:01:48  midas
   Version 2.5.5

   Revision 1.506  2004/11/01 12:26:33  midas
   Replaced 'Back' by 'List' on single entry display page

   Revision 1.505  2004/11/01 09:49:40  midas
   Made quick filter case insensitive

   Revision 1.504  2004/10/29 19:32:56  midas
   Added -m and -M command line switches

   Revision 1.503  2004/10/27 07:00:44  midas
   Version 2.5.4-6

   Revision 1.502  2004/10/26 20:37:24  midas
   Fixed problems with favicon.ico

   Revision 1.501  2004/10/26 19:13:14  midas
   Implemented 'RSS Entries'

   Revision 1.500  2004/10/26 07:29:21  midas
   Added image to RSS feed

   Revision 1.499  2004/10/25 21:26:35  midas
   Changed XML encoding

   Revision 1.498  2004/10/25 21:07:07  midas
   Implemented RSS feeds

   Revision 1.497  2004/10/14 21:27:29  midas
   Finished regular expressions

   Revision 1.496  2004/10/14 19:40:40  midas
   Fixed 'append on edit' with uploads

   Revision 1.495  2004/10/13 22:21:21  midas
   Started implementation of regex

   Revision 1.494  2004/10/13 20:15:06  midas
   Use rsputs3 for proper display of HTML logbook entries

   Revision 1.493  2004/10/13 18:21:49  midas
   Fixed compiler warning

   Revision 1.492  2004/10/13 18:18:05  midas
   Hot link can contain ';'

   Revision 1.491  2004/10/11 19:34:52  midas
   Fixed bug in xrealloc when deleting last entry

   Revision 1.490  2004/09/29 03:27:45  midas
   Added error display if disk is full

   Revision 1.489  2004/09/28 23:21:47  midas
   Version 2.5.4-5

   Revision 1.488  2004/09/28 23:18:59  midas
   Changed 'first','next' etc. to '|<', '>' etc.

   Revision 1.487  2004/09/28 23:02:23  midas
   Fixed problem with attachment submits on edit

   Revision 1.486  2004/09/24 20:49:16  midas
   Show revision on startup

   Revision 1.485  2004/09/24 20:40:25  midas
   Added cvs_revision

   Revision 1.483  2004/09/24 16:07:07  midas
   Display first/previous/next/last directly as link if first.gif is missing

   Revision 1.482  2004/09/24 00:40:57  midas
   Fixed bug with 'expand all' on logbook selection page

   Revision 1.481  2004/09/23 22:06:31  midas
   Added error when non-existing attachments get submitted

   Revision 1.480  2004/09/22 19:20:57  midas
   Fixed superfluous spaces

   Revision 1.479  2004/09/20 16:12:57  midas
   Added urs_slash_encode because of mirroring problem

   Revision 1.478  2004/09/18 05:48:33  midas
   Version 2.5.4-4

   Revision 1.477  2004/09/18 05:45:43  midas
   Fixed crash when running as windows service

   Revision 1.476  2004/09/18 04:54:17  midas
   Fixed problem withe missing attachment images

   Revision 1.475  2004/09/18 04:42:46  midas
   Fixed bug with not displaying inline images

   Revision 1.474  2004/09/18 03:27:34  midas
   Fixed problem with '/' in forgotten password

   Revision 1.473  2004/09/18 02:54:06  midas
   Removed status bar from calendar

   Revision 1.472  2004/09/18 02:43:03  midas
   Use single attribute table in edit form

   Revision 1.471  2004/09/15 05:24:31  midas
   Use again common table for attribute display

   Revision 1.470  2004/09/15 02:05:28  midas
   Only show first 1000 lines of inline ASCII attachments

   Revision 1.469  2004/09/14 22:12:47  midas
   'Use email from' has priority over user email address

   Revision 1.468  2004/09/10 14:06:42  midas
   Fixed bug with select return status

   Revision 1.467  2004/09/10 11:21:34  midas
   Version 2.5.4-3

   Revision 1.466  2004/09/09 19:35:34  midas
   Fixed bug with HUP signal going into accept()

   Revision 1.465  2004/09/08 14:41:23  midas
   Fixed crash on deleting entries during synchronization

   Revision 1.464  2004/09/08 14:31:37  midas
   Fixed bug that synchronization was not working together with 'menu commands'

   Revision 1.463  2004/09/08 12:06:18  midas
   Do not display *.ps and *.pdf files inline

   Revision 1.462  2004/09/08 10:18:37  midas
   Use rsput3 to display config page

   Revision 1.461  2004/09/08 09:36:16  midas
   Made 'move to' menu command case insensitive

   Revision 1.460  2004/08/12 19:27:59  midas
   Added more debugging info

   Revision 1.459  2004/08/11 14:03:35  midas
   Implemented possibility to serve .html files through elog

   Revision 1.458  2004/08/11 06:41:35  midas
   Release 2.5.4-2

   Revision 1.457  2004/08/10 07:51:08  midas
   Fixed bug in error display on too long content length

   Revision 1.456  2004/08/09 18:19:27  midas
   Added better error reporting

   Revision 1.455  2004/08/09 10:54:53  midas
   Fixed another memory leak

   Revision 1.454  2004/08/09 09:54:27  midas
   Fixed some (trivial) memory leaks

   Revision 1.453  2004/08/09 08:50:46  midas
   Finished hiding of attachments

   Revision 1.452  2004/08/08 20:30:10  midas
   Started adding attachment hiding

   Revision 1.451  2004/08/08 16:00:34  midas
   Renamed logf() to write_logfile()

   Revision 1.450  2004/08/08 15:32:51  midas
   Added automatic distinction between ASCII and binary files for attachment display

   Revision 1.449  2004/08/08 14:55:09  midas
   Removed all null pointer checking after xmalloc since this is now handled inside xmalloc

   Revision 1.448  2004/08/08 14:22:07  midas
   Fixed disappearing &nbsp; in replies

   Revision 1.447  2004/08/08 14:11:51  midas
   Fixed disappearing &nbsp; in config file

   Revision 1.446  2004/08/06 13:39:49  midas
   Changed CONFIG_FILE to CONFIG_PATH

   Revision 1.445  2004/08/06 07:52:51  midas
   Use macro CFGFILE instead of hard-wired elogd.cfg

   Revision 1.444  2004/08/06 07:30:52  midas
   Sorted and completed help screen

   Revision 1.443  2004/08/06 06:45:01  midas
   Added strerror in more places

   Revision 1.442  2004/08/05 09:13:18  midas
   Added check for 'Full name'

   Revision 1.441  2004/08/05 08:26:32  midas
   Changed reusing port back to old scheme

   Revision 1.440  2004/08/05 08:15:24  midas
   Changed way of reusing port

   Revision 1.439  2004/08/05 08:08:42  midas
   Fixed bug with displaying attachments

   Revision 1.438  2004/08/05 08:04:58  midas
   Added .cfg and .conf to class of text files

   Revision 1.437  2004/08/05 07:47:52  midas
   Set cookies always with path

   Revision 1.436  2004/08/04 13:45:15  midas
   Do not print 'indexing logbooks..' when running as daemon

   Revision 1.435  2004/08/04 13:43:07  midas
   eputs() makes only one call to fputs_handler

   Revision 1.434  2004/08/04 13:38:43  midas
   Removed \r for syslog

   Revision 1.433  2004/08/04 13:32:27  midas
   Fixed windows service related problems

   Revision 1.432  2004/08/04 12:38:22  midas
   Warn if port is already used

   Revision 1.431  2004/08/04 11:59:12  midas
   Added -s(ilent) option

   Revision 1.430  2004/08/04 10:34:16  midas
   Fixed compiler warning

   Revision 1.429  2004/08/04 10:32:29  midas
   Fixed compiler warning

   Revision 1.428  2004/08/04 10:30:36  midas
   Fixed password file mirror problems

   Revision 1.427  2004/08/04 09:24:17  midas
   Removed user[0] in is_admin_user()

   Revision 1.426  2004/08/04 08:33:18  midas
   Use resource dir in GetPwdFile

   Revision 1.425  2004/08/04 07:56:33  midas
   Changed 0644 to 0755 in mkdir()

   Revision 1.424  2004/08/04 07:53:43  midas
   Changed 644 to 0644

   Revision 1.423  2004/08/04 07:47:56  midas
   Added \n to error display during cloning

   Revision 1.422  2004/08/03 14:16:11  midas
   Fixed problem with {..} in attributes

   Revision 1.421  2004/08/03 09:58:36  midas
   Added 0644 to mkdir under linux

   Revision 1.420  2004/08/03 09:49:04  midas
   Added verbose output to cloning

   Revision 1.419  2004/07/30 22:45:38  midas
   Retrieve password files during cloning

   Revision 1.418  2004/07/30 20:47:57  midas
   Fixed wrong link for deleting entries during synchronization

   Revision 1.417  2004/07/30 19:51:10  midas
   Improved error reporting by Recai Oktas

   Revision 1.416  2004/07/30 13:41:40  midas
   Fixed compiler warning

   Revision 1.415  2004/07/30 08:06:46  midas
   Improved error display with synchronization

   Revision 1.414  2004/07/30 07:12:59  midas
   Made p<attribute>=value work again

   Revision 1.413  2004/07/28 19:53:28  midas
   Increased possible size of 'welcome title' to 10000 chars

   Revision 1.412  2004/07/28 19:20:46  midas
   Fixed wrong date on date attributes on edit/reply if attribute is fixed

   Revision 1.411  2004/07/28 18:51:48  midas
   Close syslog on cleanup

   Revision 1.410  2004/07/28 13:24:06  midas
   Fixed problem that attachments could not be deleted

   Revision 1.409  2004/07/28 12:52:30  midas
   Added int_vasprintf from Recai Oktas

   Revision 1.408  2004/07/28 12:00:38  midas
   Fixed bug with invalid sizeof(list)

   Revision 1.407  2004/07/28 10:05:07  midas
   Fixed open anchor

   Revision 1.406  2004/07/27 20:48:49  midas
   Fixed compiler warnings

   Revision 1.405  2004/07/27 20:37:08  midas
   Implemented read_password

   Revision 1.404  2004/07/23 23:27:26  midas
   Finished entry deletion with confirmation

   Revision 1.403  2004/07/23 23:06:59  midas
   Fixed problem with delete recommendation

   Revision 1.402  2004/07/23 22:49:02  midas
   Improved cloning message display

   Revision 1.401  2004/07/23 22:28:17  midas
   Cloning now works with password access

   Revision 1.400  2004/07/23 19:23:51  midas
   Supply full path to elogd.cfg when running elogd as windows service

   Revision 1.399  2004/07/23 09:42:39  midas
   Use onUnload only if locking enabled

   Revision 1.398  2004/07/23 07:42:57  midas
   Fixed bug in extract_host()

   Revision 1.397  2004/07/23 07:31:32  midas
   Fixed typo

   Revision 1.396  2004/07/23 07:30:26  midas
   Fixed compiler warnings

   Revision 1.395  2004/07/23 06:59:06  midas
   Fixed missing argument to getcfg() under linux

   Revision 1.394  2004/07/22 21:05:29  midas
   Fixed bugs with https:// in URL

   Revision 1.393  2004/07/22 20:51:53  midas
   Added size parameter to getcfg()

   Revision 1.392  2004/07/22 20:23:39  midas
   Message handling implemented by Recai Oktas

   Revision 1.391  2004/07/15 20:40:33  midas
   Return error number in retrieve_remote_md5()

   Revision 1.390  2004/07/15 19:56:00  midas
   Implemented 'max content length'

   Revision 1.389  2004/07/15 19:15:47  midas
   Implemented 'preset on reply'

   Revision 1.388  2004/07/15 10:01:03  midas
   First attribute cannot be on same line

   Revision 1.387  2004/07/15 09:55:03  midas
   Use 'format <attrib> = 1' also in entry form

   Revision 1.386  2004/07/15 07:59:15  midas
   Apply tooltip title to whole attribute row

   Revision 1.385  2004/07/14 20:29:40  midas
   Implemented 'tooltip <attribute>'

   Revision 1.384  2004/07/14 15:20:13  midas
   Fixed bug in el_enum_attr()

   Revision 1.383  2004/07/14 14:25:48  midas
   Don't evaluate 'preset xxx' on replies

   Revision 1.382  2004/07/14 10:10:59  midas
   Fixed compiler warning

   Revision 1.381  2004/07/14 10:09:47  midas
   Fixed bug in stristr()

   Revision 1.380  2004/07/13 21:04:37  midas
   Implemented synchronizing during cloning

   Revision 1.379  2004/07/12 20:36:48  midas
   Started to implement synchronize after clone

   Revision 1.378  2004/07/12 13:52:49  midas
   Fixed problem with options list containing several quotation marks

   Revision 1.377  2004/07/12 08:01:15  midas
   Added 'fix text' flag

   Revision 1.376  2004/07/09 08:13:39  midas
   Fixed problem that admin user of top group could change global section

   Revision 1.375  2004/07/08 19:53:14  midas
   Fixed wrong link with 'show top groups' flag

   Revision 1.374  2004/07/08 19:48:09  midas
   Implemented 'show top groups' flag

   Revision 1.373  2004/07/08 11:31:33  midas
   Fixed compiler warning

   Revision 1.372  2004/07/08 11:30:29  midas
   Fixed string overflow in rsputs2()

   Revision 1.371  2004/07/07 15:53:11  midas
   Added error display if max. number of attribute options gets exceeded

   Revision 1.370  2004/07/07 15:10:50  midas
   Omit 'preset text' on edit of entries

   Revision 1.369  2004/07/07 14:29:06  midas
   Added note about 'stealing' of locks

   Revision 1.368  2004/07/07 13:51:44  midas
   Admin user has to supply own old password

   Revision 1.367  2004/07/07 13:27:09  midas
   Fixed problem of password change when using crypt()

   Revision 1.366  2004/07/07 13:21:58  midas
   Removed puts(0)

   Revision 1.365  2004/07/07 11:59:41  midas
   Added $utcdate

   Revision 1.364  2004/07/05 08:51:33  midas
   Fixed typo

   Revision 1.363  2004/07/05 08:49:18  midas
   Fixed compiler warnings

   Revision 1.362  2004/06/29 18:57:23  midas
   Made rename/create logbook work with groups

   Revision 1.361  2004/06/28 20:29:49  midas
   Rename logbook in groups on logbook rename

   Revision 1.360  2004/06/28 19:35:07  midas
   Rename logbook directory on logbook rename

   Revision 1.359  2004/06/27 12:16:37  midas
   Implemented renaming of logbooks

   Revision 1.358  2004/06/25 18:50:52  midas
   Do a el_index_logbooks on HUP signal

   Revision 1.357  2004/06/23 08:04:10  midas
   Fixed small bug with 'X-Forwarded-Host:'

   Revision 1.356  2004/06/23 08:00:26  midas
   Extract and use 'X-Forwarded-Host:'

   Revision 1.355  2004/06/23 07:41:34  midas
   Added _cmdline in redirection

   Revision 1.354  2004/06/23 07:34:04  midas
   Redirect login screen if URL does not match the one in the config file

   Revision 1.353  2004/06/21 19:11:35  midas
   Implemented retrieve_elog_from

   Revision 1.352  2004/06/21 18:04:43  midas
   Fixed email notification problem if URL does not contain a trailing '/'

   Revision 1.351  2004/06/18 20:44:37  midas
   Implemented deletion of logbooks through web interface

   Revision 1.350  2004/06/18 19:26:33  midas
   Fixed typo

   Revision 1.349  2004/06/18 19:19:44  midas
   Fixed typo

   Revision 1.348  2004/06/18 19:09:24  midas
   Made substituions work correctly with date attributes

   Revision 1.347  2004/06/17 15:16:34  midas
   Fixed bug with password recovery

   Revision 1.346  2004/06/16 12:27:00  midas
   Submit (unlock) unmodified entry upon unload

   Revision 1.345  2004/06/16 12:15:58  midas
   Mark re-edited entries as modified in jscript

   Revision 1.344  2004/06/15 20:58:15  midas
   Fixed bug with extendable attributes and onunload() checking

   Revision 1.343  2004/06/15 20:52:14  midas
   Implemented first version of onunload() checking for abandoned edits

   Revision 1.342  2004/06/14 11:59:28  midas
   Added support for read password during cloning

   Revision 1.341  2004/06/14 11:13:19  midas
   Added error handling for cloning

   Revision 1.340  2004/06/14 10:59:13  midas
   Added server code for cloning

   Revision 1.339  2004/06/14 10:58:51  midas
   Added client code for cloning

   Revision 1.338  2004/06/11 07:14:50  midas
   Tested onUnload

   Revision 1.337  2004/06/07 14:59:42  midas
   Fixed problem with 'preset text' under conditional attributes

   Revision 1.336  2004/06/07 10:36:48  midas
   Version 2.5.3

   Revision 1.335  2004/06/05 21:45:55  midas
   Added NT service functionality

   Revision 1.334  2004/06/04 22:20:20  midas
   Added missing translations

   Revision 1.333  2004/06/04 22:11:36  midas
   Added missing loc() dummies

   Revision 1.332  2004/06/04 21:37:14  midas
   Fixed substring problem with admin user

   Revision 1.331  2004/06/04 15:05:01  midas
   Adjusted indentation

   Revision 1.330  2004/06/04 14:51:40  midas
   Implemented stristr

   Revision 1.329  2004/06/04 14:03:15  midas
   Fixed stack overflow under Windows on resubmit of entry

   Revision 1.328  2004/05/21 13:02:51  midas
   Fixed bug with date attributes and 'subst on edit'

   Revision 1.327  2004/05/19 20:27:50  midas
   Added <hr> and <br> to be recognized as HTML in attributes

   Revision 1.326  2004/05/18 21:46:08  midas
   Allow ',' and '.' and '-' for numeric format

   Revision 1.325  2004/05/11 09:16:03  midas
   Fixed typo

   Revision 1.324  2004/05/10 19:46:07  midas
   Extendable options can now reside also in [globa] section

   Revision 1.323  2004/05/10 09:34:27  midas
   Fixed compiler warning

   Revision 1.322  2004/05/08 15:02:01  midas
   Fixed bug with remove _all_ leading to infinite page forwarding

   Revision 1.321  2004/05/05 15:26:04  midas
   Fixed compiler warning

   Revision 1.320  2004/05/05 15:24:45  midas
   Changed redirection to absolute location

   Revision 1.319  2004/04/30 22:17:29  midas
   Implemented text body CSV import

   Revision 1.318  2004/04/08 14:21:14  midas
   Implemented 'protect selection page'

   Revision 1.317  2004/04/06 21:19:59  midas
   Fixed problem with multiple quick filters on attributes without option lists

   Revision 1.316  2004/03/27 14:33:59  midas
   Implemeted first version of favicon

   Revision 1.315  2004/03/26 08:28:29  midas
   Version 2.5.2

   Revision 1.314  2004/03/25 20:33:47  midas
   *** empty log message ***

   Revision 1.313  2004/03/25 19:47:48  midas
   Fixed problem with attributes containing spaces

   Revision 1.312  2004/03/24 20:44:21  midas
   Improved synchronization speed

   Revision 1.311  2004/03/22 21:13:07  midas
   Improved speed of strieq()

   Revision 1.310  2004/03/22 14:17:31  midas
   Implemented 'display <attribute>' for single entry display

   Revision 1.309  2004/03/22 14:14:57  midas
   Implemented 'display <attribute>'

   Revision 1.308  2004/03/22 10:30:37  midas
   Added CSV (;) export

   Revision 1.307  2004/03/21 20:01:48  midas
   Version 2.5.2 beta

   Revision 1.306  2004/03/21 19:25:18  midas
   Implemented XML encoding

   Revision 1.305  2004/03/21 18:46:31  midas
   Removed size parameter in csv_import

   Revision 1.304  2004/03/21 18:42:59  midas
   Implemented CSV import and XML export

   Revision 1.303  2004/03/19 15:11:20  midas
   Implemented 'guest list display'

   Revision 1.302  2004/03/19 11:12:00  midas
   Implemented 'extendable options' for MOptions

   Revision 1.301  2004/03/19 10:30:56  midas
   Fixed bug with topgroup initialization

   Revision 1.300  2004/03/17 21:11:04  midas
   Removed debug print

   <oder revision comments removed>

\********************************************************************/

/* Version of ELOG */
#define VERSION "2.6.0-beta3"
char cvs_revision[] = "$Id$";

/* ELOG identification */
static const char ELOGID[] = "elogd " VERSION " built " __DATE__ ", " __TIME__;

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <assert.h>
#include <locale.h>

/* Default name of the configuration file. */
#ifndef CFGFILE
#define CFGFILE "elogd.cfg"
#endif

/* Default TCP port for server. */
#ifndef DEFAULT_PORT
#define DEFAULT_PORT 80
#endif

#ifdef _MSC_VER

#define OS_WINNT

#define DIR_SEPARATOR '\\'
#define DIR_SEPARATOR_STR "\\"

#define snprintf _snprintf

#include <windows.h>
#include <io.h>
#include <conio.h>
#include <time.h>
#include <direct.h>
#include <sys/stat.h>

#else

#define OS_UNIX

#define TRUE 1
#define FALSE 0

#ifndef O_TEXT
#define O_TEXT 0
#define O_BINARY 0
#endif

#define DIR_SEPARATOR '/'
#define DIR_SEPARATOR_STR "/"

#ifndef DEFAULT_USER
#define DEFAULT_USER "nobody"
#endif

#ifndef DEFAULT_GROUP
#define DEFAULT_GROUP "nogroup"
#endif

#ifndef PIDFILE
#define PIDFILE "/var/run/elogd.pid"
#endif

#ifndef __USE_XOPEN
#define __USE_XOPEN             /* needed for crypt() */
#endif

typedef int BOOL;

#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <dirent.h>
#include <errno.h>
#include <ctype.h>
#include <pwd.h>
#include <grp.h>
#include <syslog.h>
#include <termios.h>

#define closesocket(s) close(s)

#ifndef stricmp
#define stricmp(s1, s2) strcasecmp(s1, s2)
#endif

gid_t orig_gid;                 /* Original effective GID before dropping privilege */
uid_t orig_uid;                 /* Original effective UID before dropping privilege */
char pidfile[256];              /* Pidfile name                                     */

#endif                          /* OS_UNIX */

/* local includes */
#include "regex.h"
#include "mxml.h"
#include "strlcpy.h"

BOOL running_as_daemon;         /* Running as a daemon/service? */
int elog_tcp_port = (int) DEFAULT_PORT; /* Server's TCP port            */

static void (*printf_handler) (const char *);   /* Handler to printf for logging */
static void (*fputs_handler) (const char *);    /* Handler to fputs for logging  */
static FILE *current_output_stream = NULL;      /* Currently used output stream  */

#define SYSLOG_PRIORITY LOG_NOTICE      /* Default priority for syslog facility */

typedef int INT;

#define TELL(fh) lseek(fh, 0, SEEK_CUR)

#ifdef OS_WINNT
#define TRUNCATE(fh) chsize(fh, TELL(fh))
#else
#define TRUNCATE(fh) ftruncate(fh, TELL(fh))
#endif

#define NAME_LENGTH  1500

#define DEFAULT_TIME_FORMAT "%c"
#define DEFAULT_DATE_FORMAT "%x"

#define DEFAULT_HTTP_CHARSET "ISO-8859-1"

#define SUCCESS        1
#define FAILURE        0

#define EL_SUCCESS     1
#define EL_FIRST_MSG   2
#define EL_LAST_MSG    3
#define EL_NO_MSG      4
#define EL_FILE_ERROR  5
#define EL_UPGRADE     6
#define EL_EMPTY       7
#define EL_MEM_ERROR   8
#define EL_DUPLICATE   9
#define EL_INVAL_FILE 10

#define EL_FIRST       1
#define EL_LAST        2
#define EL_NEXT        3
#define EL_PREV        4

char *return_buffer;
int return_buffer_size;
int strlen_retbuf;
int keep_alive;
char header_buffer[20000];
int return_length;
char host_name[256];
char referer[256];
char browser[256];
char config_file[256];
char resource_dir[256];
char logbook_dir[256];
char listen_interface[256];
char theme_name[80];
char http_host[256];

#define MAX_GROUPS       32
#define MAX_PARAM       120
#define MAX_ATTACHMENTS  50
#define MAX_N_LIST      100
#define MAX_N_ATTR      100
#define MAX_REPLY_TO    100
#define CMD_SIZE      10000
#define TEXT_SIZE    250000
#define MAX_PATH_LENGTH 256

#define MAX_CONTENT_LENGTH 10*1024*1024

char _param[MAX_PARAM][NAME_LENGTH];
char _value[MAX_PARAM][NAME_LENGTH];
char _mtext[TEXT_SIZE];
char _cmdline[CMD_SIZE];
char *_attachment_buffer;
INT _attachment_size;
INT _max_content_length = MAX_CONTENT_LENGTH;
struct in_addr rem_addr;
char rem_host[256];
INT _sock;
BOOL verbose, use_keepalive, enable_execute = FALSE;
INT _current_message_id;
INT _logging_level;

char *mname[] = {
   "January",
   "February",
   "March",
   "April",
   "May",
   "June",
   "July",
   "August",
   "September",
   "October",
   "November",
   "December"
};

char type_list[MAX_N_LIST][NAME_LENGTH] = {
   "Routine",
   "Other"
};

char category_list[MAX_N_LIST][NAME_LENGTH] = {
   "General",
   "Other",
};

char author_list[MAX_N_LIST][NAME_LENGTH] = {
   ""
};

/* attribute flags */
#define AF_REQUIRED           (1<<0)
#define AF_LOCKED             (1<<1)
#define AF_MULTI              (1<<2)
#define AF_FIXED_EDIT         (1<<3)
#define AF_FIXED_REPLY        (1<<4)
#define AF_ICON               (1<<5)
#define AF_RADIO              (1<<6)
#define AF_EXTENDABLE         (1<<7)
#define AF_DATE               (1<<8)
#define AF_DATETIME           (1<<9)
#define AF_TIME              (1<<10)
#define AF_NUMERIC           (1<<11)
#define AF_USERLIST          (1<<12)

/* attribute format flags */
#define AFF_SAME_LINE              1
#define AFF_MULTI_LINE             2
#define AFF_DATE                   4
#define AFF_EXTENDABLE             8

char attr_list[MAX_N_ATTR][NAME_LENGTH];
char attr_options[MAX_N_ATTR][MAX_N_LIST][NAME_LENGTH];
int attr_flags[MAX_N_ATTR];

char attr_list_default[][NAME_LENGTH] = {
   "Author",
   "Type",
   "Category",
   "Subject",
   ""
};

char attr_options_default[][MAX_N_LIST][NAME_LENGTH] = {
   {""},
   {"Routine", "Other"},
   {"General", "Other"},
   {""}
};

int attr_flags_default[] = {
   AF_REQUIRED,
   0,
   0,
   0
};

struct {
   char ext[32];
   char type[32];
} filetype[] = {

   {
   ".AI", "application/postscript"}, {
   ".ASC", "text/plain"}, {
   ".BZ2", "application/x-bzip2"}, {
   ".CFG", "text/plain"}, {
   ".CHRT", "application/x-kchart"}, {
   ".CONF", "text/plain"}, {
   ".CSH", "application/x-csh"}, {
   ".CSS", "text/css"}, {
   ".DOC", "application/msword"}, {
   ".DVI", "application/x-dvi"}, {
   ".EPS", "application/postscript"}, {
   ".GIF", "image/gif"}, {
   ".GZ", "application/x-gzip"}, {
   ".HTM", "text/html"}, {
   ".HTML", "text/html"}, {
   ".ICO", "image/x-icon"}, {
   ".JPEG", "image/jpeg"}, {
   ".JPG", "image/jpeg"}, {
   ".JS", "application/x-javascript"}, {
   ".KPR", "application/x-kpresenter"}, {
   ".KSP", "application/x-kspread"}, {
   ".KWD", "application/x-kword"}, {
   ".MP3", "audio/mpeg"}, {
   ".OGG", "application/x-ogg"}, {
   ".PDF", "application/pdf"}, {
   ".PNG", "image/png"}, {
   ".PS", "application/postscript"}, {
   ".RAM", "audio/x-pn-realaudio"}, {
   ".RM", "audio/x-pn-realaudio"}, {
   ".RM", "audio/x-pn-realaudio"}, {
   ".RM", "audio/x-pn-realaudio"}, {
   ".RPM", "application/x-rpm"}, {
   ".RTF", "application/rtf"}, {
   ".SH", "application/x-sh"}, {
   ".TAR", "application/x-tar"}, {
   ".TCL", "application/x-tcl"}, {
   ".TEX", "application/x-tex"}, {
   ".TGZ", "application/x-gzip"}, {
   ".TIF", "image/tiff"}, {
   ".TIFF", "image/tiff"}, {
   ".TXT", "text/plain"}, {
   ".WAV", "audio/x-wav"}, {
   ".XLS", "application/x-msexcel"}, {
   ".XML", "text/xml"}, {
   ".XSL", "text/xml"}, {
   ".ZIP", "application/x-zip-compressed"}, {

   ".THUMB", "image/jpeg"}, {   // hard-coded for now...
"", ""},};

typedef struct {
   int message_id;
   char file_name[32];
   time_t file_time;
   int offset;
   int in_reply_to;
   unsigned char md5_digest[16];
} EL_INDEX;

typedef struct {
   char name[256];
   char name_enc[256];
   char data_dir[256];
   char top_group[256];
   EL_INDEX *el_index;
   int *n_el_index;
   int n_attr;
   PMXML_NODE pwd_xml_tree;
} LOGBOOK;

typedef struct {
   int message_id;
   unsigned char md5_digest[16];
} MD5_INDEX;

typedef struct LBNODE *LBLIST;

struct LBNODE {
   char name[256];
   LBLIST *member;
   int n_members;
   int is_top;
} LBNODE;

typedef struct {
   LOGBOOK *lbs;
   int index;
   char string[256];
   int in_reply_to;
} MSG_LIST;

LOGBOOK *lb_list = NULL;

#ifdef OS_WINNT
int run_service(void);
#endif

void show_error(char *error);
BOOL enum_user_line(LOGBOOK * lbs, int n, char *user, int size);
int get_user_line(LOGBOOK * lbs, char *user, char *password, char *full_name,
                  char *email, BOOL email_notify[1000], time_t * last_access);
int strbreak(char *str, char list[][NAME_LENGTH], int size, char *brk);
int execute_shell(LOGBOOK * lbs, int message_id, char attrib[MAX_N_ATTR][NAME_LENGTH],
                  char att_file[MAX_ATTACHMENTS][256], char *sh_cmd);
BOOL isparam(char *param);
char *getparam(char *param);
void write_logfile(LOGBOOK * lbs, const char *format, ...);
BOOL check_login_user(LOGBOOK * lbs, char *user);
LBLIST get_logbook_hierarchy(void);
BOOL is_logbook_in_group(LBLIST pgrp, char *logbook);
BOOL is_admin_user(char *logbook, char *user);
BOOL is_admin_user_global(char *user);
void free_logbook_hierarchy(LBLIST root);
void show_top_text(LOGBOOK * lbs);
void show_bottom_text(LOGBOOK * lbs);
int set_attributes(LOGBOOK * lbs, char attributes[][NAME_LENGTH], int n);
void show_elog_list(LOGBOOK * lbs, INT past_n, INT last_n, INT page_n, char *info);
int change_config_line(LOGBOOK * lbs, char *option, char *old_value, char *new_value);
int read_password(char *pwd, int size);
int getcfg(char *group, char *param, char *value, int vsize);
int build_subst_list(LOGBOOK * lbs, char list[][NAME_LENGTH], char value[][NAME_LENGTH],
                     char attrib[][NAME_LENGTH], BOOL format_date);
void highlight_searchtext(regex_t * re_buf, char *src, char *dst, BOOL hidden);
int parse_config_file(char *config_file);
PMXML_NODE load_password_file(LOGBOOK * lbs, char *error, int error_size);
int load_password_files();
void compose_base_url(LOGBOOK * lbs, char *base_url, int size);
void show_elog_entry(LOGBOOK * lbs, char *dec_path, char *command);
char *loc(char *orig);
void strencode(char *text);
int scan_attributes(char *logbook);

/*---- Funcions from the MIDAS library -----------------------------*/

#define my_toupper(_c)    ( ((_c)>='a' && (_c)<='z') ? ((_c)-'a'+'A') : (_c) )

BOOL strieq(const char *str1, const char *str2)
{
   char c1, c2;

   if (str1 == NULL && str2 == NULL)
      return TRUE;
   if (str1 == NULL || str2 == NULL)
      return FALSE;
   if (strlen(str1) != strlen(str2))
      return FALSE;

   while (*str1) {
      c1 = *str1++;
      c2 = *str2++;
      if (my_toupper(c1) != my_toupper(c2))
         return FALSE;
   }

   if (*str2)
      return FALSE;

   return TRUE;
}

BOOL strnieq(const char *str1, const char *str2, int n)
{
   char c1, c2;
   int i;

   if (str1 == NULL && str2 == NULL && n == 0)
      return TRUE;
   if (str1 == NULL || str2 == NULL)
      return FALSE;
   if ((int) strlen(str1) < n || (int) strlen(str2) < n)
      return FALSE;

   for (i = 0; i < n && *str1; i++) {
      c1 = *str1++;
      c2 = *str2++;
      if (my_toupper(c1) != my_toupper(c2))
         return FALSE;
   }

   if (i < n)
      return FALSE;

   return TRUE;
}

char *stristr(const char *str, const char *pattern)
{
   char c1, c2, *ps, *pp;

   if (str == NULL || pattern == NULL)
      return NULL;

   while (*str) {
      ps = (char *) str;
      pp = (char *) pattern;
      c1 = *ps;
      c2 = *pp;
      if (my_toupper(c1) == my_toupper(c2)) {
         while (*pp) {
            c1 = *ps++;
            c2 = *pp++;

            if (my_toupper(c1) != my_toupper(c2))
               break;
         }

         if (!*pp)
            return (char *) str;
      }
      str++;
   }

   return NULL;
}

void strextract(const char *str, char delim, char *extr, int size)
/* extract a substinr "extr" from "str" until "delim" is found */
{
   int i;

   for (i = 0; str[i] != delim && i < size - 1; i++)
      extr[i] = str[i];
   extr[i] = 0;
}

static BOOL chkext(const char *str, const char *ext)
{
   int extl, strl;
   char c1, c2;

   if (ext == NULL || str == NULL)
      return FALSE;

   extl = strlen(ext);
   strl = strlen(str);
   if (extl >= strl)
      return FALSE;
   str = str + strl - extl;
   while (*str) {
      c1 = *str++;
      c2 = *ext++;
      if (my_toupper(c1) != my_toupper(c2))
         return FALSE;
   }
   return TRUE;
}

/* workaroud for some gcc versions bug for "%c" format (see strftime(3) */
size_t my_strftime(char *s, size_t max, const char *fmt, const struct tm * tm)
{
   return strftime(s, max, fmt, tm);
}

/*---- Safe malloc wrappers with out of memory checking from GNU ---*/

static void memory_error_and_abort(char *func)
{
   extern void eprintf(const char *, ...);

   eprintf("%s: not enough memory\n", func);
   exit(EXIT_FAILURE);
}

/* Return a pointer to free()able block of memory large enough
   to hold BYTES number of bytes.  If the memory cannot be allocated,
   print an error message and abort. */
void *xmalloc(size_t bytes)
{
   void *temp;

   temp = malloc(bytes);
   if (temp == 0)
      memory_error_and_abort("xmalloc");
   return (temp);
}

void *xcalloc(size_t count, size_t bytes)
{
   void *temp;

   temp = calloc(count, bytes);
   if (temp == 0)
      memory_error_and_abort("xcalloc");
   return (temp);
}

void *xrealloc(void *pointer, size_t bytes)
{
   void *temp;

   temp = pointer ? realloc(pointer, bytes) : malloc(bytes);

   if (temp == 0)
      memory_error_and_abort("xrealloc");
   return (temp);
}

void xfree(void *pointer)
{
   if (pointer)
      free(pointer);
}

char *xstrdup(const char *string)
{
   char *s;

   s = (char *) xmalloc(strlen(string) + 1);
   strcpy(s, string);
   return s;
}

/*----------------------- Message handling -------------------------*/

/* Have vasprintf? (seems that only libc6 based Linux has this) */
#ifdef __linux__
#  define HAVE_VASPRINTF
#endif

#ifndef HAVE_VASPRINTF
/* vasprintf implementation taken (and adapted) from GNU libiberty */

static int int_vasprintf(char **result, const char *format, va_list args)
{
   const char *p = format;
   /* Add one to make sure that it is never zero, which might cause malloc
      to return NULL.  */
   int total_width = strlen(format) + 1;
   va_list ap;

#ifdef va_copy
   va_copy(ap, args);
#else
   memcpy(&ap, &args, sizeof(va_list));
#endif

   while (*p != '\0') {
      if (*p++ == '%') {
         while (strchr("-+ #0", *p))
            ++p;
         if (*p == '*') {
            ++p;
            total_width += abs(va_arg(ap, int));
         } else
            total_width += strtoul(p, (char **) &p, 10);
         if (*p == '.') {
            ++p;
            if (*p == '*') {
               ++p;
               total_width += abs(va_arg(ap, int));
            } else
               total_width += strtoul(p, (char **) &p, 10);
         }
         while (strchr("hlL", *p))
            ++p;
         /*
          * Should be big enough for any format specifier
          * except %s and floats.
          */
         total_width += 30;
         switch (*p) {
         case 'd':
         case 'i':
         case 'o':
         case 'u':
         case 'x':
         case 'X':
         case 'c':
            (void) va_arg(ap, int);
            break;
         case 'f':
         case 'e':
         case 'E':
         case 'g':
         case 'G':
            (void) va_arg(ap, double);
            /*
             * Since an ieee double can have an exponent of 307, we'll
             * make the buffer wide enough to cover the gross case.
             */
            total_width += 307;
            break;
         case 's':
            total_width += strlen(va_arg(ap, char *));
            break;
         case 'p':
         case 'n':
            (void) va_arg(ap, char *);
            break;
         }
         p++;
      }
   }
#ifdef va_copy
   va_end(ap);
#endif
   *result = (char *) malloc(total_width);
   if (*result != NULL)
      return vsprintf(*result, format, args);
   else
      return -1;
}

#if defined (_BSD_VA_LIST_) && defined (__FreeBSD__)
int vasprintf(char **result, const char *format, _BSD_VA_LIST_ args)
#else
int vasprintf(char **result, const char *format, va_list args)
#endif
{
   return int_vasprintf(result, format, args);
}
#endif                          /* ! HAVE_VASPRINTF */

/* Safe replacement for vasprintf (adapted code from Samba) */
int xvasprintf(char **ptr, const char *format, va_list ap)
{
   int n;
   va_list save;

#ifdef va_copy
   va_copy(save, ap);
#else
# ifdef __va_copy
   __va_copy(save, ap);
# else
   save = ap;
# endif
#endif

   n = vasprintf(ptr, format, save);

   if (n == -1 || !*ptr) {
      printf("Not enough memory");
      exit(EXIT_FAILURE);
   }

   return n;
}

/* Driver for printf_handler, drop-in replacement for printf */
void eprintf(const char *format, ...)
{
   va_list ap;
   char *msg;

   va_start(ap, format);
   xvasprintf(&msg, format, ap);
   va_end(ap);

   (*printf_handler) (msg);

   free(msg);
}

/* Driver for fputs_handler, drop-in replacement for fputs(buf, fd) */
void efputs(const char *buf)
{
   (*fputs_handler) (buf);
}

/* Dump with the newline, drop-in replacement for puts(buf) */
void eputs(const char *buf)
{
   char *p;

   p = xmalloc(strlen(buf) + 2);
   strcpy(p, buf);
   strcat(p, "\n");

   (*fputs_handler) (p);

   xfree(p);
}

/* Flush the current output stream */
void eflush(void)
{
   /* Do this only for non-NULL streams (uninitiated stream or a syslog) */
   if (current_output_stream != NULL)
      fflush(current_output_stream);
}

#ifdef OS_WINNT
HANDLE hEventLog;
#endif

/* Print MSG to syslog */
void print_syslog(const char *msg)
{
   char *p;

   /* strip trailing \r and \n */
   p = xstrdup(msg);
   while (p[strlen(p) - 1] == '\r' || p[strlen(p) - 1] == '\n')
      p[strlen(p) - 1] = 0;

#ifdef OS_UNIX
   syslog(SYSLOG_PRIORITY, "%s", p);
#else
   ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, &p, NULL);
#endif

   xfree(p);
}

/* Print MSG to stderr */
void print_stderr(const char *msg)
{
   fprintf(stderr, "%s", msg);
}

/* Dump BUF to syslog */
void fputs_syslog(const char *buf)
{
   char *p;

   /* strip trailing \r and \n */
   p = xstrdup(buf);
   while (p[strlen(p) - 1] == '\r' || p[strlen(p) - 1] == '\n')
      p[strlen(p) - 1] = 0;

#ifdef OS_UNIX
   syslog(SYSLOG_PRIORITY, "%s", p);
#else
   ReportEvent(hEventLog, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, &p, NULL);
#endif

   xfree(p);
}

/* Dump BUF to stderr */
void fputs_stderr(const char *buf)
{
   fputs(buf, stderr);
}

/* Redirect all messages handled with eprintf/efputs
   to syslog (Unix) or event log (Windows) */
void redirect_to_syslog(void)
{
   static int has_inited = 0;

   /* initiate syslog */
   if (!has_inited) {
#ifdef OS_UNIX
      setlogmask(LOG_UPTO(SYSLOG_PRIORITY));
      openlog("elogd", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);
#else
      hEventLog = RegisterEventSource(NULL, "ELOG");
#endif
   }
   has_inited = 1;

   printf_handler = print_syslog;
   fputs_handler = fputs_syslog;

   /* tells that the syslog facility is currently used as output */
   current_output_stream = NULL;
}

/* Redirect all messages handled with eprintf/efputs to stderr */
void redirect_to_stderr(void)
{
   printf_handler = print_stderr;
   fputs_handler = fputs_stderr;

   current_output_stream = stderr;
}

/*------------------------------------------------------------------*/

void strsubst(char *string, int size, char name[][NAME_LENGTH], char value[][NAME_LENGTH], int n)
/* subsitute "$name" with value corresponding to name */
{
   int i, j;
   char tmp[2 * NAME_LENGTH], str[2 * NAME_LENGTH], uattr[2 * NAME_LENGTH], *ps, *pt, *p;

   pt = tmp;
   ps = string;
   for (p = strchr(ps, '$'); p != NULL; p = strchr(ps, '$')) {
      /* copy leading characters */
      j = (int) (p - ps);
      if (j >= (int) sizeof(tmp))
         return;
      memcpy(pt, ps, j);
      pt += j;
      p++;

      /* extract name */
      strlcpy(str, p, sizeof(str));
      for (j = 0; j < (int) strlen(str); j++)
         str[j] = toupper(str[j]);

      /* search name */
      for (i = 0; i < n; i++) {
         strlcpy(uattr, name[i], sizeof(uattr));
         for (j = 0; j < (int) strlen(uattr); j++)
            uattr[j] = toupper(uattr[j]);

         if (strncmp(str, uattr, strlen(uattr)) == 0)
            break;
      }

      /* copy value */
      if (i < n) {
         strlcpy(pt, value[i], sizeof(tmp) - ((int) pt - (int) tmp));
         pt += strlen(pt);
         ps = p + strlen(uattr);
      } else {
         *pt++ = '$';
         ps = p;
      }
   }

   /* copy remainder */
   strlcpy(pt, ps, sizeof(tmp) - ((int) pt - (int) tmp));

   /* return result */
   strlcpy(string, tmp, size);
}

/*------------------------------------------------------------------*/

void url_decode(char *p)
/********************************************************************\
Decode the given string in-place by expanding %XX escapes
\********************************************************************/
{
   char *pD, str[3];
   int i;

   pD = p;
   while (*p) {
      if (*p == '%') {
         /* Escape: next 2 chars are hex representation of the actual character */
         p++;
         if (isxdigit(p[0]) && isxdigit(p[1])) {
            str[0] = p[0];
            str[1] = p[1];
            str[2] = 0;
            sscanf(p, "%02X", &i);

            *pD++ = (char) i;
            p += 2;
         } else
            *pD++ = '%';
      } else if (*p == '+') {
         /* convert '+' to ' ' */
         *pD++ = ' ';
         p++;
      } else {
         *pD++ = *p++;
      }
   }
   *pD = '\0';
}

void url_encode(char *ps, int size)
/********************************************************************\
Encode the given string in-place by adding %XX escapes
\********************************************************************/
{
   unsigned char *pd, *p;
   char str[NAME_LENGTH];

   pd = (unsigned char *) str;
   p = (unsigned char *) ps;
   while (*p && (int) pd < (int) str + 250) {
      if (strchr("%&=#?+", *p) || *p > 127) {
         sprintf((char *) pd, "%%%02X", *p);
         pd += 3;
         p++;
      } else if (*p == ' ') {
         *pd++ = '+';
         p++;
      } else {
         *pd++ = *p++;
      }
   }
   *pd = '\0';
   strlcpy(ps, str, size);
}

void url_slash_encode(char *ps, int size)
/********************************************************************\
Do the same including '/' characters
\********************************************************************/
{
   unsigned char *pd, *p;
   char str[NAME_LENGTH];

   pd = (unsigned char *) str;
   p = (unsigned char *) ps;
   while (*p && (int) pd < (int) str + 250) {
      if (strchr("%&=#?+/", *p) || *p > 127) {
         sprintf((char *) pd, "%%%02X", *p);
         pd += 3;
         p++;
      } else if (*p == ' ') {
         *pd++ = '+';
         p++;
      } else {
         *pd++ = *p++;
      }
   }
   *pd = '\0';
   strlcpy(ps, str, size);
}

/*-------------------------------------------------------------------*/

void str_escape(char *ps, int size)
/********************************************************************\
Encode the given string in-place by adding \\ escapes for `$"\
\********************************************************************/
{
   unsigned char *pd, *p;
   char str[NAME_LENGTH];

   pd = (unsigned char *) str;
   p = (unsigned char *) ps;
   while (*p && (int) pd < (int) str + 250) {
      if (strchr("'$\"\\", *p)) {
         *pd++ = '\\';
         *pd++ = *p++;
      } else {
         *pd++ = *p++;
      }
   }
   *pd = '\0';
   strlcpy(ps, str, size);
}

void btou(char *str)
/* convert all blanks to underscores in a string */
{
   int i;

   for (i = 0; i < (int) strlen(str); i++)
      if (str[i] == ' ')
         str[i] = '_';
}

/*-------------------------------------------------------------------*/

void dtou(char *str)
/* convert all dots to underscores in a string */
{
   int i;

   for (i = 0; i < (int) strlen(str); i++)
      if (str[i] == '.')
         str[i] = '_';
}

/*-------------------------------------------------------------------*/

char *map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

int cind(char c)
{
   int i;

   if (c == '=')
      return 0;

   for (i = 0; i < 64; i++)
      if (map[i] == c)
         return i;

   return -1;
}

void base64_decode(char *s, char *d)
{
   unsigned int t;

   while (s && *s) {
      t = cind(*s) << 18;
      s++;
      t |= cind(*s) << 12;
      s++;
      t |= cind(*s) << 6;
      s++;
      t |= cind(*s) << 0;
      s++;

      *(d + 2) = (char) (t & 0xFF);
      t >>= 8;
      *(d + 1) = (char) (t & 0xFF);
      t >>= 8;
      *d = (char) (t & 0xFF);

      d += 3;
   }
   *d = 0;
}

void base64_encode(unsigned char *s, unsigned char *d)
{
   unsigned int t, pad;

   pad = 3 - strlen((char *)s) % 3;
   if (pad == 3)
      pad = 0;
   while (*s) {
      t = (*s++) << 16;
      if (*s)
         t |= (*s++) << 8;
      if (*s)
         t |= (*s++) << 0;

      *(d + 3) = map[t & 63];
      t >>= 6;
      *(d + 2) = map[t & 63];
      t >>= 6;
      *(d + 1) = map[t & 63];
      t >>= 6;
      *(d + 0) = map[t & 63];

      d += 4;
   }
   *d = 0;
   while (pad--)
      *(--d) = '=';
}

void base64_bufenc(unsigned char *s, int len, char *d)
{
   unsigned int t, pad;
   int i;

   pad = 3 - len % 3;
   if (pad == 3)
      pad = 0;
   for (i = 0; i < len;) {
      t = s[i++] << 16;
      if (i < len)
         t |= s[i++] << 8;
      if (i < len)
         t |= s[i++] << 0;

      *(d + 3) = map[t & 63];
      t >>= 6;
      *(d + 2) = map[t & 63];
      t >>= 6;
      *(d + 1) = map[t & 63];
      t >>= 6;
      *(d + 0) = map[t & 63];

      d += 4;
   }
   *d = 0;
   while (pad--)
      *(--d) = '=';
}

void do_crypt(char *s, char *d)
{
#ifdef HAVE_CRYPT
   strcpy(d, crypt(s, "el"));
#else
   base64_encode((unsigned char *) s, (unsigned char *) d);
#endif
}

/*------------------------------------------------------------------*\

   MD5 Checksum Routines

\*------------------------------------------------------------------*/

typedef struct {
   unsigned int state[4];       // state (ABCD)
   unsigned int count[2];       // number of bits, modulo 2^64 (lsb first)
   unsigned char buffer[64];    // input buffer
} MD5_CONTEXT;

/*------------------------------------------------------------------*/

/* prototypes of the support routines */
void _MD5_update(MD5_CONTEXT *, const void *, unsigned int);
void _MD5_transform(unsigned int[4], unsigned char[64]);
void _MD5_encode(unsigned char *, unsigned int *, unsigned int);
void _MD5_decode(unsigned int *, unsigned char *, unsigned int);

/* F, G, H and I are basic MD5 functions */
#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
#define H(x, y, z) ((x) ^ (y) ^ (z))
#define I(x, y, z) ((y) ^ ((x) | (~z)))

/* ROTATE_LEFT rotates x left n bits */
#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))

/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4 */
/* Rotation is separate from addition to prevent recomputation */
#define FF(a, b, c, d, x, s, ac) {                  \
   (a) += F ((b), (c), (d)) + (x) + (unsigned int)(ac);  \
   (a) = ROTATE_LEFT ((a), (s));                   \
   (a) += (b);                                     \
                                                   }
#define GG(a, b, c, d, x, s, ac) {                  \
   (a) += G ((b), (c), (d)) + (x) + (unsigned int)(ac);  \
   (a) = ROTATE_LEFT ((a), (s));                   \
   (a) += (b);                                     \
                                                   }
#define HH(a, b, c, d, x, s, ac) {                  \
   (a) += H ((b), (c), (d)) + (x) + (unsigned int)(ac);  \
   (a) = ROTATE_LEFT ((a), (s));                   \
   (a) += (b);                                     \
                                                   }
#define II(a, b, c, d, x, s, ac) {                  \
   (a) += I ((b), (c), (d)) + (x) + (unsigned int)(ac);  \
   (a) = ROTATE_LEFT ((a), (s));                   \
   (a) += (b);                                     \
                                                   }

/*------------------------------------------------------------------*/

/* main MD5 checksum routine, returns digest from pdata buffer */

void MD5_checksum(const void *pdata, unsigned int len, unsigned char digest[16])
{
   MD5_CONTEXT ctx;
   unsigned char bits[8];
   unsigned int i, padlen;

   /* to allow multithreading we have to locate the padding memory here */
   unsigned char PADDING[64] = {
      0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
   };

   memset(&ctx, 0, sizeof(MD5_CONTEXT));
   ctx.count[0] = ctx.count[1] = 0;

   /* load magic initialization constants */
   ctx.state[0] = 0x67452301;
   ctx.state[1] = 0xefcdab89;
   ctx.state[2] = 0x98badcfe;
   ctx.state[3] = 0x10325476;

   _MD5_update(&ctx, pdata, len);

   // save number of bits
   _MD5_encode(bits, ctx.count, 8);

   // pad out to 56 mod 64
   i = (unsigned int) ((ctx.count[0] >> 3) & 0x3f);
   padlen = (i < 56) ? (56 - i) : (120 - i);
   _MD5_update(&ctx, PADDING, padlen);

   // append length (before padding)
   _MD5_update(&ctx, bits, 8);

   // store state in digest
   _MD5_encode(digest, ctx.state, 16);
}

/*------------------------------------------------------------------*/

void _MD5_update(MD5_CONTEXT * pctx, const void *pdata, unsigned int len)
{
   unsigned char *pin;
   unsigned int i, index, partlen;

   pin = (unsigned char *) pdata;

   // compute number of bytes mod 64
   index = (unsigned int) ((pctx->count[0] >> 3) & 0x3F);

   // update number of bits
   if ((pctx->count[0] += ((unsigned int) len << 3)) < ((unsigned int) len << 3))
      pctx->count[1]++;
   pctx->count[1] += ((unsigned int) len >> 29);

   partlen = 64 - index;

   // transform as many times as possible.
   if (len >= partlen) {
      memcpy(&pctx->buffer[index], pin, partlen);
      _MD5_transform(pctx->state, pctx->buffer);

      for (i = partlen; i + 63 < len; i += 64)
         _MD5_transform(pctx->state, &pin[i]);

      index = 0;
   } else
      i = 0;

   /* buffer remaining input */
   memcpy(&pctx->buffer[index], &pin[i], len - i);
}

/*------------------------------------------------------------------*/

/* basic transformation, transforms state based on block */
void _MD5_transform(unsigned int state[4], unsigned char block[64])
{
   unsigned int lA = state[0], lB = state[1], lC = state[2], lD = state[3];
   unsigned int x[16];

   _MD5_decode(x, block, 64);

   /* round 1 */
   FF(lA, lB, lC, lD, x[0], 7, 0xd76aa478);     // 1
   FF(lD, lA, lB, lC, x[1], 12, 0xe8c7b756);    // 2
   FF(lC, lD, lA, lB, x[2], 17, 0x242070db);    // 3
   FF(lB, lC, lD, lA, x[3], 22, 0xc1bdceee);    // 4
   FF(lA, lB, lC, lD, x[4], 7, 0xf57c0faf);     // 5
   FF(lD, lA, lB, lC, x[5], 12, 0x4787c62a);    // 6
   FF(lC, lD, lA, lB, x[6], 17, 0xa8304613);    // 7
   FF(lB, lC, lD, lA, x[7], 22, 0xfd469501);    // 8
   FF(lA, lB, lC, lD, x[8], 7, 0x698098d8);     // 9
   FF(lD, lA, lB, lC, x[9], 12, 0x8b44f7af);    // 10
   FF(lC, lD, lA, lB, x[10], 17, 0xffff5bb1);   // 11
   FF(lB, lC, lD, lA, x[11], 22, 0x895cd7be);   // 12
   FF(lA, lB, lC, lD, x[12], 7, 0x6b901122);    // 13
   FF(lD, lA, lB, lC, x[13], 12, 0xfd987193);   // 14
   FF(lC, lD, lA, lB, x[14], 17, 0xa679438e);   // 15
   FF(lB, lC, lD, lA, x[15], 22, 0x49b40821);   // 16

   /* round 2 */
   GG(lA, lB, lC, lD, x[1], 5, 0xf61e2562);     // 17
   GG(lD, lA, lB, lC, x[6], 9, 0xc040b340);     // 18
   GG(lC, lD, lA, lB, x[11], 14, 0x265e5a51);   // 19
   GG(lB, lC, lD, lA, x[0], 20, 0xe9b6c7aa);    // 20
   GG(lA, lB, lC, lD, x[5], 5, 0xd62f105d);     // 21
   GG(lD, lA, lB, lC, x[10], 9, 0x2441453);     // 22
   GG(lC, lD, lA, lB, x[15], 14, 0xd8a1e681);   // 23
   GG(lB, lC, lD, lA, x[4], 20, 0xe7d3fbc8);    // 24
   GG(lA, lB, lC, lD, x[9], 5, 0x21e1cde6);     // 25
   GG(lD, lA, lB, lC, x[14], 9, 0xc33707d6);    // 26
   GG(lC, lD, lA, lB, x[3], 14, 0xf4d50d87);    // 27
   GG(lB, lC, lD, lA, x[8], 20, 0x455a14ed);    // 28
   GG(lA, lB, lC, lD, x[13], 5, 0xa9e3e905);    // 29
   GG(lD, lA, lB, lC, x[2], 9, 0xfcefa3f8);     // 30
   GG(lC, lD, lA, lB, x[7], 14, 0x676f02d9);    // 31
   GG(lB, lC, lD, lA, x[12], 20, 0x8d2a4c8a);   // 32

   /* round 3 */
   HH(lA, lB, lC, lD, x[5], 4, 0xfffa3942);     // 33
   HH(lD, lA, lB, lC, x[8], 11, 0x8771f681);    // 34
   HH(lC, lD, lA, lB, x[11], 16, 0x6d9d6122);   // 35
   HH(lB, lC, lD, lA, x[14], 23, 0xfde5380c);   // 36
   HH(lA, lB, lC, lD, x[1], 4, 0xa4beea44);     // 37
   HH(lD, lA, lB, lC, x[4], 11, 0x4bdecfa9);    // 38
   HH(lC, lD, lA, lB, x[7], 16, 0xf6bb4b60);    // 39
   HH(lB, lC, lD, lA, x[10], 23, 0xbebfbc70);   // 40
   HH(lA, lB, lC, lD, x[13], 4, 0x289b7ec6);    // 41
   HH(lD, lA, lB, lC, x[0], 11, 0xeaa127fa);    // 42
   HH(lC, lD, lA, lB, x[3], 16, 0xd4ef3085);    // 43
   HH(lB, lC, lD, lA, x[6], 23, 0x4881d05);     // 44
   HH(lA, lB, lC, lD, x[9], 4, 0xd9d4d039);     // 45
   HH(lD, lA, lB, lC, x[12], 11, 0xe6db99e5);   // 46
   HH(lC, lD, lA, lB, x[15], 16, 0x1fa27cf8);   // 47
   HH(lB, lC, lD, lA, x[2], 23, 0xc4ac5665);    // 48

   /* round 4 */
   II(lA, lB, lC, lD, x[0], 6, 0xf4292244);     // 49
   II(lD, lA, lB, lC, x[7], 10, 0x432aff97);    // 50
   II(lC, lD, lA, lB, x[14], 15, 0xab9423a7);   // 51
   II(lB, lC, lD, lA, x[5], 21, 0xfc93a039);    // 52
   II(lA, lB, lC, lD, x[12], 6, 0x655b59c3);    // 53
   II(lD, lA, lB, lC, x[3], 10, 0x8f0ccc92);    // 54
   II(lC, lD, lA, lB, x[10], 15, 0xffeff47d);   // 55
   II(lB, lC, lD, lA, x[1], 21, 0x85845dd1);    // 56
   II(lA, lB, lC, lD, x[8], 6, 0x6fa87e4f);     // 57
   II(lD, lA, lB, lC, x[15], 10, 0xfe2ce6e0);   // 58
   II(lC, lD, lA, lB, x[6], 15, 0xa3014314);    // 59
   II(lB, lC, lD, lA, x[13], 21, 0x4e0811a1);   // 60
   II(lA, lB, lC, lD, x[4], 6, 0xf7537e82);     // 61
   II(lD, lA, lB, lC, x[11], 10, 0xbd3af235);   // 62
   II(lC, lD, lA, lB, x[2], 15, 0x2ad7d2bb);    // 63
   II(lB, lC, lD, lA, x[9], 21, 0xeb86d391);    // 64

   state[0] += lA;
   state[1] += lB;
   state[2] += lC;
   state[3] += lD;

   /* lClear sensitive information */
   memset(x, 0, sizeof(x));
}

/*------------------------------------------------------------------*/

/* encodes input (unsigned int) into output (unsigned char),
   assumes that lLen is a multiple of 4 */
void _MD5_encode(unsigned char *pout, unsigned int *pin, unsigned int len)
{
   unsigned int i, j;

   for (i = 0, j = 0; j < len; i++, j += 4) {
      pout[j] = (unsigned char) (pin[i] & 0x0ff);
      pout[j + 1] = (unsigned char) ((pin[i] >> 8) & 0x0ff);
      pout[j + 2] = (unsigned char) ((pin[i] >> 16) & 0x0ff);
      pout[j + 3] = (unsigned char) ((pin[i] >> 24) & 0x0ff);
   }
}

/*------------------------------------------------------------------*/

/* encodes input (unsigned char) into output (unsigned int),
   assumes that lLen is a multiple of 4 */
void _MD5_decode(unsigned int *pout, unsigned char *pin, unsigned int len)
{
   unsigned int i, j;

   for (i = 0, j = 0; j < len; i++, j += 4)
      pout[i] = ((unsigned int) pin[j]) |
          (((unsigned int) pin[j + 1]) << 8) | (((unsigned int) pin[j + 2]) << 16) |
          (((unsigned int) pin[j + 3]) << 24);
}

/*------------------------------------------------------------------*/

BOOL file_exist(char *file_name)
{
   int fh;

   fh = open(file_name, O_RDONLY);
   if (fh < 0)
      return FALSE;

   close(fh);
   return TRUE;
}

/*------------------------------------------------------------------*/

void serialdate2date(double days, int *day, int *month, int *year)
/* convert days since 1.1.1900 to date */
{
   int i, j, l, n;

   l = (int) days + 68569 + 2415019;
   n = (int) ((4 * l) / 146097);
   l = l - (int) ((146097 * n + 3) / 4);
   i = (int) ((4000 * (l + 1)) / 1461001);
   l = l - (int) ((1461 * i) / 4) + 31;
   j = (int) ((80 * l) / 2447);
   *day = l - (int) ((2447 * j) / 80);
   l = (int) (j / 11);
   *month = j + 2 - (12 * l);
   *year = 100 * (n - 49) + i + l;
}

double date2serialdate(int day, int month, int year)
/* convert date to days since 1.1.1900 */
{
   int serialdate;

   serialdate = (int) ((1461 * (year + 4800 + (int) ((month - 14) / 12))) / 4) +
       (int) ((367 * (month - 2 - 12 * ((month - 14) / 12))) / 12) -
       (int) ((3 * ((int) ((year + 4900 + (int) ((month - 14) / 12)) / 100))) / 4) + day - 2415019 - 32075;

   return serialdate;
}

/*------------------------------------------------------------------*/

/* Wrapper for setegid. */
int setgroup(char *str)
{
#ifdef OS_UNIX
   struct group *gr;

   gr = getgrnam(str);

   if (gr != NULL)
      if (setegid(gr->gr_gid) >= 0 && initgroups(gr->gr_name, gr->gr_gid) >= 0)
         return 0;
      else {
         eprintf("Cannot set effective GID to group \"%s\"\n", gr->gr_name);
         eprintf("setgroup: %s\n", strerror(errno));
   } else
      eprintf("Group \"%s\" not found\n", str);

   return -1;
#else
   return 0;
#endif
}

                                                   /* Wrapper for seteuid. */
int setuser(char *str)
{
#ifdef OS_UNIX
   struct passwd *pw;

   pw = getpwnam(str);

   if (pw != NULL)
      if (seteuid(pw->pw_uid) >= 0)
         return 0;
      else {
         eprintf("Cannot set effective UID to user \"%s\"\n", str);
         eprintf("setuser: %s\n", strerror(errno));
   } else
      eprintf("User \"%s\" not found\n", str);

   return -1;
#else
   return 0;
#endif
}

/*-------------------------------------------------------------------*/

INT recv_string(int sock, char *buffer, INT buffer_size, INT millisec)
{
   INT i, n;
   fd_set readfds;
   struct timeval timeout;

   n = 0;
   memset(buffer, 0, buffer_size);

   do {
      if (millisec > 0) {
         FD_ZERO(&readfds);
         FD_SET(sock, &readfds);

         timeout.tv_sec = millisec / 1000;
         timeout.tv_usec = (millisec % 1000) * 1000;

         select(FD_SETSIZE, (void *) &readfds, NULL, NULL, (void *) &timeout);

         if (!FD_ISSET(sock, &readfds))
            break;
      }

      i = recv(sock, buffer + n, 1, 0);

      if (i <= 0)
         break;

      n++;

      if (n >= buffer_size)
         break;

   } while (buffer[n - 1] && buffer[n - 1] != 10);

   return n - 1;
}

/*-------------------------------------------------------------------*/

int check_smtp_error(char *str, int expected, char *error, int error_size)
{
   if (atoi(str) != expected) {
      strlcpy(error, str + 4, error_size);
      return 0;
   }

   return 1;
}

/*-------------------------------------------------------------------*/

INT sendmail(LOGBOOK * lbs, char *smtp_host, char *from, char *to,
             char *subject, char *text, BOOL email_to, char *url, char *content_type,
             char att_file[MAX_ATTACHMENTS][256], char *error, int error_size)
{
   struct sockaddr_in bind_addr;
   struct hostent *phe;
   int i, n, s, offset, strsize, n_att, index, fh;
   char buf[80];
   char *str, *p, boundary[256], file_name[MAX_PATH_LENGTH];
   time_t now;
   struct tm *ts;
   char list[1024][NAME_LENGTH], buffer[256], charset[256], subject_enc[2000], decoded[256];

   memset(error, 0, error_size);

   if (verbose)
      eprintf("\n\nEmail from %s to %s, SMTP host %s:\n", from, to, smtp_host);
   write_logfile(lbs, "Email from %s to %s, SMTP host %s:\n", from, to, smtp_host);

   if (!getcfg("global", "charset", charset, sizeof(charset)))
      strcpy(charset, DEFAULT_HTTP_CHARSET);

   /* count attachments */
   n_att = 0;
   if (att_file)
      for (n_att = 0; att_file[n_att][0] && n_att < MAX_ATTACHMENTS; n_att++);

   /* create a new socket for connecting to remote server */
   s = socket(AF_INET, SOCK_STREAM, 0);
   if (s == -1)
      return -1;

   /* connect to remote node port 25 */
   memset(&bind_addr, 0, sizeof(bind_addr));
   bind_addr.sin_family = AF_INET;
   bind_addr.sin_port = htons((short) 25);

   phe = gethostbyname(smtp_host);
   if (phe == NULL) {
      strcpy(error, loc("Cannot lookup server name"));
      return 1;
   }
   memcpy((char *) &(bind_addr.sin_addr), phe->h_addr, phe->h_length);

   if (connect(s, (void *) &bind_addr, sizeof(bind_addr)) < 0) {
      closesocket(s);
      strcpy(error, loc("Cannot connect to server"));
      return 1;
   }

   strsize = TEXT_SIZE + 1000;
   str = xmalloc(strsize);

   recv_string(s, str, strsize, 10000);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   /* drain server messages */
   do {
      str[0] = 0;
      recv_string(s, str, strsize, 300);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
   } while (str[0]);

   if (getcfg(lbs->name, "SMTP username", str, strsize)) {

      snprintf(str, strsize - 1, "EHLO %s\r\n", host_name);
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      do {
         recv_string(s, str, strsize, 3000);
         if (verbose)
            efputs(str);
         write_logfile(lbs, str);
         if (!check_smtp_error(str, 250, error, error_size))
            goto smtp_error;
      } while (stristr(str, " OK") == NULL);

   } else {

      snprintf(str, strsize - 1, "HELO %s\r\n", host_name);

      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
      recv_string(s, str, strsize, 3000);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
      if (!check_smtp_error(str, 250, error, error_size))
         goto smtp_error;
   }

   /* optional authentication */
   if (getcfg(lbs->name, "SMTP username", str, strsize)) {

      snprintf(str, strsize - 1, "AUTH LOGIN\r\n");
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
      recv_string(s, str, strsize, 3000);
      if (strchr(str, '\r'))
         *strchr(str, '\r') = 0;
      base64_decode(str + 4, decoded);
      strcat(decoded, "\n");
      if (verbose)
         efputs(decoded);
      write_logfile(lbs, decoded);
      if (!check_smtp_error(str, 334, error, error_size))
         goto smtp_error;

      getcfg(lbs->name, "SMTP username", decoded, sizeof(decoded));
      base64_encode((unsigned char *) decoded, (unsigned char *) str);
      strcat(str, "\r\n");
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
      recv_string(s, str, strsize, 3000);
      if (strchr(str, '\r'))
         *strchr(str, '\r') = 0;
      base64_decode(str + 4, decoded);
      strcat(decoded, "\n");
      if (verbose)
         efputs(decoded);
      write_logfile(lbs, decoded);
      if (!check_smtp_error(str, 334, error, error_size))
         goto smtp_error;

      getcfg(lbs->name, "SMTP password", str, strsize);
      strcat(str, "\r\n");
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
      recv_string(s, str, strsize, 3000);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
      if (!check_smtp_error(str, 235, error, error_size))
         goto smtp_error;
   }

   snprintf(str, strsize - 1, "MAIL FROM: %s\r\n", from);
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);
   recv_string(s, str, strsize, 3000);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   if (!check_smtp_error(str, 250, error, error_size))
      goto smtp_error;

   /* break recipients into list */
   n = strbreak(to, list, 1024, ",");

   for (i = 0; i < n; i++) {
      snprintf(str, strsize - 1, "RCPT TO: <%s>\r\n", list[i]);
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      /* increased timeout for SMTP servers with long alias lists */
      recv_string(s, str, strsize, 30000);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      if (!check_smtp_error(str, 250, error, error_size))
         goto smtp_error;
   }

   snprintf(str, strsize - 1, "DATA\r\n");
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);
   recv_string(s, str, strsize, 3000);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);
   if (!check_smtp_error(str, 354, error, error_size))
      goto smtp_error;

   if (email_to)
      snprintf(str, strsize - 1, "To: %s\r\n", to);
   else
      snprintf(str, strsize - 1, "To: ELOG\r\n");

   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   strcpy(subject_enc, "=?");
   strlcat(subject_enc, charset, sizeof(subject_enc));
   strlcat(subject_enc, "?B?", sizeof(subject_enc));
   base64_encode((unsigned char *) subject,
                 (unsigned char *) (subject_enc + strlen(subject_enc)));
   strlcat(subject_enc, "?=", sizeof(subject_enc));

   snprintf(str, strsize - 1, "From: %s\r\nSubject: %s\r\n", from, subject_enc);
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   snprintf(str, strsize - 1, "X-Mailer: Elog, Version %s\r\n", VERSION);
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   if (url) {
      snprintf(str, strsize - 1, "X-Elog-URL: %s\r\n", url);
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
   }

   snprintf(str, strsize - 1, "X-Elog-submit-type: web|elog\r\n");
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   /* switch locale temporarily back to english to comply with RFC2822 date format */
   setlocale(LC_ALL, "C");

   time(&now);
   ts = localtime(&now);
   strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S", ts);
   offset = (-(int) timezone);
   if (ts->tm_isdst)
      offset += 3600;
   if (verbose) {
      snprintf(str, strsize - 1, "timezone: %d, offset: %d\n", (int) timezone, (int) offset);
      efputs(str);
   }
   snprintf(str, strsize - 1, "Date: %s %+03d%02d\r\n", buf, (int) (offset / 3600),
            (int) ((abs((int) offset) / 60) % 60));
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   getcfg("global", "Language", str, sizeof(str));
   if (str[0])
      setlocale(LC_ALL, str);

   if (n_att > 0) {
      snprintf(str, strsize - 1, "MIME-Version: 1.0\r\n");
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      sprintf(boundary, "%04X-%04X=:%04X", rand(), rand(), rand());
      snprintf(str, strsize - 1, "Content-Type: MULTIPART/MIXED; BOUNDARY=\"%s\"\r\n\r\n", boundary);
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      snprintf(str, strsize - 1,
               "  This message is in MIME format.  The first part should be readable text,\r\n");
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      snprintf(str, strsize - 1,
               "  while the remaining parts are likely unreadable without MIME-aware tools.\r\n\r\n");
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      snprintf(str, strsize - 1, "--%s\r\nContent-Type: %s; charset=\"%s\"\r\n\r\n",
               boundary, content_type, charset);

      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
   } else {
      snprintf(str, strsize - 1, "Content-Type: %s; charset=\"%s\"\r\n\r\n", content_type, charset);
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);
   }

   /* analyze text for "." at beginning of line */
   p = text;
   str[0] = 0;
   while (strstr(p, "\r\n.\r\n")) {
      i = (int) strstr(p, "\r\n.\r\n") - (int) p + 1;
      strlcat(str, p, i);
      p += i + 4;
      strlcat(str, "\r\n..\r\n", strsize);
   }
   strlcat(str, p, strsize);
   strlcat(str, "\r\n\r\n", strsize);
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);

   if (n_att > 0) {
      snprintf(str, strsize - 1, "--%s\r\n", boundary);
      send(s, str, strlen(str), 0);
      if (verbose)
         efputs(str);
      write_logfile(lbs, str);

      for (index = 0; index < n_att; index++) {
         /* return proper Content-Type for file type */
         for (i = 0; i < (int) strlen(att_file[index]); i++)
            str[i] = toupper(att_file[index][i]);
         str[i] = 0;

         for (i = 0; filetype[i].ext[0]; i++)
            if (strstr(str, filetype[i].ext))
               break;

         if (filetype[i].ext[0])
            snprintf(str, strsize - 1, "Content-Type: %s; name=\"%s\"\r\n",
                     filetype[i].type, att_file[index] + 14);
         else if (strchr(str, '.') == NULL)
            snprintf(str, strsize - 1, "Content-Type: text/plain; name=\"%s\"\r\n", att_file[index] + 14);
         else
            snprintf(str, strsize - 1,
                     "Content-Type: application/octet-stream; name=\"%s\"\r\n", att_file[index] + 14);

         send(s, str, strlen(str), 0);
         if (verbose)
            efputs(str);
         write_logfile(lbs, str);

         snprintf(str, strsize - 1, "Content-Transfer-Encoding: BASE64\r\n");
         send(s, str, strlen(str), 0);
         if (verbose)
            efputs(str);
         write_logfile(lbs, str);

         snprintf(str, strsize - 1,
                  "Content-Disposition: attachment; filename=\"%s\"\r\n\r\n", att_file[index] + 14);
         send(s, str, strlen(str), 0);
         if (verbose)
            efputs(str);
         write_logfile(lbs, str);

         /* encode file */
         strlcpy(file_name, lbs->data_dir, sizeof(file_name));
         strlcat(file_name, att_file[index], sizeof(file_name));

         fh = open(file_name, O_RDONLY | O_BINARY);
         if (fh > 0) {
            do {
               n = read(fh, buffer, 45);
               if (n <= 0)
                  break;

               base64_bufenc((unsigned char *) buffer, n, str);
               strcat(str, "\r\n");
               send(s, str, strlen(str), 0);
               if (verbose)
                  efputs(str);
            } while (1);

            close(fh);
         }

         /* send boundary */
         if (index < n_att - 1)
            snprintf(str, strsize - 1, "\r\n--%s\r\n", boundary);
         else
            snprintf(str, strsize - 1, "\r\n--%s--\r\n", boundary);

         send(s, str, strlen(str), 0);
         if (verbose)
            efputs(str);
         write_logfile(lbs, str);
      }
   }

   /* send ".<CR>" to signal end of message */
   snprintf(str, strsize - 1, ".\r\n");
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);

   recv_string(s, str, strsize, 3000);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);
   if (!check_smtp_error(str, 250, error, error_size))
      goto smtp_error;

   snprintf(str, strsize - 1, "QUIT\r\n");
   send(s, str, strlen(str), 0);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);
   recv_string(s, str, strsize, 3000);
   if (verbose)
      efputs(str);
   write_logfile(lbs, str);
   if (!check_smtp_error(str, 221, error, error_size))
      goto smtp_error;

 smtp_error:

   closesocket(s);
   xfree(str);

   return 1;
}

/*-------------------------------------------------------------------*/

void split_url(char *url, char *host, int *port, char *subdir, char *param)
{
   char *p, str[256];

   if (host) host[0] = 0;
   if (port) *port = 80;
   if (subdir) subdir[0] = 0;
   if (param) param[0] = 0;

   p = url;
   if (strncmp(url, "http://", 7) == 0)
      p += 7;
   if (strncmp(url, "https://", 8) == 0)
      p += 8;

   strncpy(str, p, sizeof(str));
   if (strchr(str, '/')) {
      if (subdir)
         strncpy(subdir, strchr(str, '/'), 256);
      *strchr(str, '/') = 0;
   }

   if (strchr(str, '?')) {
      if (subdir)
         strncpy(subdir, strchr(str, '?'), 256);
      *strchr(str, '?') = 0;
   }

   if (strchr(str, ':')) {
      if (port)
         *port = atoi(strchr(str, ':') + 1);
      *strchr(str, ':') = 0;
   }

   if (host)
      strcpy(host, str);

   if (subdir) {
      if (strchr(subdir, '?')) {
         strncpy(param, strchr(subdir, '?'), 256);
         *strchr(subdir, '?') = 0;
      }

      if (subdir[0] == 0)
         strcpy(subdir, "/");
   }
}

/*-------------------------------------------------------------------*/

int retrieve_url(char *url, char **buffer, char *rpwd)
{
   struct sockaddr_in bind_addr;
   struct hostent *phe;
   char str[256], host[256], subdir[256], param[256], auth[256], pwd_enc[256];
   int port, bufsize;
   INT i, n;
   fd_set readfds;
   struct timeval timeout;

   static int sock, last_port;
   static char last_host[256];

   *buffer = NULL;
   split_url(url, host, &port, subdir, param);

   if (sock && (strcmp(host, last_host) != 0 || port != last_port)) {
      closesocket(sock);
      sock = 0;
   }

   if (sock) {                  // keep-alive does not yet work, requires evaluation of Content-Length !!!
      closesocket(sock);
      sock = 0;
   }

   /* create a new socket for connecting to remote server */
   if (!sock) {

      sock = socket(AF_INET, SOCK_STREAM, 0);
      if (sock == -1)
         return -1;

      /* connect to remote node */
      memset(&bind_addr, 0, sizeof(bind_addr));
      bind_addr.sin_family = AF_INET;
      bind_addr.sin_port = htons((short) port);

      phe = gethostbyname(host);
      if (phe == NULL)
         return -1;
      memcpy((char *) &(bind_addr.sin_addr), phe->h_addr, phe->h_length);

      if (connect(sock, (void *) &bind_addr, sizeof(bind_addr)) < 0) {
         closesocket(sock);
         return -1;
      }
   }

   last_port = port;
   strcpy(last_host, host);

   /* compose GET request, avoid chunked data in HTTP/1.1 protocol */
   sprintf(str, "GET %s%s HTTP/1.0\r\nConnection: Close\r\n", subdir, param);

   /* add local username/password */
   if (isparam("unm"))
      sprintf(str + strlen(str), "Cookie: unm=%s; upwd=%s\r\n", getparam("unm"), getparam("upwd"));

   if (rpwd && rpwd[0]) {
      sprintf(auth, "anybody:%s", rpwd);
      base64_encode((unsigned char *) auth, (unsigned char *) pwd_enc);
      sprintf(str + strlen(str), "Authorization: Basic %s\r\n", pwd_enc);
   }

   /* add host (RFC2616, Sec. 14) */
   sprintf(str + strlen(str), "Host: %s:%d\r\n", host, port);

   /* add keepalive */
   //sprintf(str + strlen(str), "Keep-Alive: 600\r\n");
   //sprintf(str + strlen(str), "Connection: keep-alive\r\n");

   strcat(str, "\r\n");

   send(sock, str, strlen(str), 0);

   bufsize = TEXT_SIZE + 1000;
   *buffer = xmalloc(bufsize);
   memset(*buffer, 0, bufsize);

   n = 0;

   do {
      FD_ZERO(&readfds);
      FD_SET(sock, &readfds);

      timeout.tv_sec = 30;      /* 30 sec. timeout */
      timeout.tv_usec = 0;

      select(FD_SETSIZE, (void *) &readfds, NULL, NULL, (void *) &timeout);

      if (!FD_ISSET(sock, &readfds)) {
         closesocket(sock);
         sock = 0;
         xfree(*buffer);
         *buffer = NULL;
         return -1;
      }

      i = recv(sock, *buffer + n, bufsize - n, 0);

      if (i <= 0)
         break;

      n += i;

      if (n >= bufsize) {
         /* increase buffer size */
         bufsize += 10000;
         *buffer = xrealloc(*buffer, bufsize);
         if (*buffer == NULL) {
            closesocket(sock);
            return -1;
         }
      }

   } while (1);

   return n;
}

/*-------------------------------------------------------------------*/

INT ss_daemon_init()
{
#ifdef OS_UNIX

   /* only implemented for UNIX */
   int i, fd, pid;

   if ((pid = fork()) < 0)
      return FAILURE;
   else if (pid != 0)
      _exit(EXIT_SUCCESS);      /* parent finished, exit without atexit hook */

   /* child continues here */

   /* try and use up stdin, stdout and stderr, so other
      routines writing to stdout etc won't cause havoc. Copied from smbd */
   for (i = 0; i < 3; i++) {
      close(i);
      fd = open("/dev/null", O_RDWR, 0);
      if (fd < 0)
         fd = open("/dev/null", O_WRONLY, 0);
      if (fd < 0) {
         eprintf("Can't open /dev/null");
         return FAILURE;
      }
      if (fd != i) {
         eprintf("Did not get file descriptor");
         return FAILURE;
      }
   }

   setsid();                    /* become session leader */
   chdir("/");                  /* change working direcotry (not on NFS!) */
   umask(0);                    /* clear our file mode creation mask */

#endif

   return SUCCESS;
}

/*------------------------------------------------------------------*/

/* Parameter extraction from configuration file similar to win.ini */

typedef struct {
   char *param;
   char *uparam;
   char *value;
} CONFIG_PARAM;

typedef struct {
   char *section_name;
   int n_params;
   CONFIG_PARAM *config_param;
} LB_CONFIG;

LB_CONFIG *lb_config = NULL;
int n_lb_config = 0;

char _topgroup[256];
char _condition[256];
time_t cfgfile_mtime = 0;

/*-------------------------------------------------------------------*/

void check_config_file(BOOL force)
{
   struct stat cfg_stat;

   if (force) {
      parse_config_file(config_file);
      return;
   }

   /* force re-read configuration file if changed */
   if (stat(config_file, &cfg_stat) == 0) {
      if (cfgfile_mtime < cfg_stat.st_mtime) {
         cfgfile_mtime = cfg_stat.st_mtime;
         parse_config_file(config_file);
      }
   } else
      eprintf("Cannot stat() config file \"%s\": %s\n", config_file, strerror(errno));
}

/*-------------------------------------------------------------------*/

void setcfg_topgroup(char *topgroup)
{
   strcpy(_topgroup, topgroup);
}

char *getcfg_topgroup()
{
   if (_topgroup[0])
      return _topgroup;

   return NULL;
}

/*------------------------------------------------------------------*/

int is_logbook(char *logbook)
{
   char str[256];

   strlcpy(str, logbook, sizeof(str));
   str[6] = 0;
   return !strieq(str, "global");
}

/*-------------------------------------------------------------------*/

void set_condition(char *c)
{
   strlcpy(_condition, c, sizeof(_condition));
}

/*-------------------------------------------------------------------*/

void evaluate_conditions(LOGBOOK *lbs, char attrib[MAX_N_ATTR][NAME_LENGTH])
{
   char condition[256], str[256];
   int index, i;

   condition[0] = 0;
   set_condition("");
   for (index = 0; index < lbs->n_attr; index++) {
      for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++) {

         if (strchr(attr_options[index][i], '{') && strchr(attr_options[index][i], '}')) {

            strlcpy(str, attr_options[index][i], sizeof(str));
            *strchr(str, '{') = 0;

            if (strieq(str, attrib[index])) {
               strlcpy(str, strchr(attr_options[index][i], '{') + 1, sizeof(str));
               if (*strchr(str, '}'))
                  *strchr(str, '}') = 0;

               if (condition[0] == 0)
                  strlcpy(condition, str, sizeof(condition));
               else {
                  strlcat(condition, ",", sizeof(condition));
                  strlcat(condition, str, sizeof(condition));
               }

               set_condition(condition);
               scan_attributes(lbs->name);
            }
         }
      }
   }
}

/*-------------------------------------------------------------------*/

BOOL match_param(char *str, char *param, int conditional_only)
{
   int ncl, npl, nand, i, j, k;
   char *p, pcond[256], clist[10][NAME_LENGTH], plist[10][NAME_LENGTH], alist[10][NAME_LENGTH];

   if (conditional_only && str[0] != '{')
      return FALSE;

   if (!_condition[0] || str[0] != '{')
      return (strcmp(str, param) == 0);

   p = str;
   if (strchr(p, '}'))
      p = strchr(p, '}') + 1;
   while (*p == ' ')
      p++;

   strlcpy(pcond, str, sizeof(pcond));
   if (strchr(pcond, '}'))
      *strchr(pcond, '}') = 0;
   if (strchr(pcond, '{'))
      *strchr(pcond, '{') = ' ';

   npl = strbreak(pcond, plist, 10, ",");
   ncl = strbreak(_condition, clist, 10, ",");

   for (i = 0; i < ncl; i++)
      for (j = 0; j < npl; j++)
         if (stricmp(clist[i], plist[j]) == 0) {
            /* condition matches */
            return strcmp(p, param) == 0;
         }

   /* check and'ed conditions */
   for (i = 0; i < npl; i++)
      if (strchr(plist[i], '&')) {
         nand = strbreak(plist[i], alist, 10, "&");
         for (j = 0; j < nand; j++) {
            for (k = 0; k < ncl; k++)
               if (stricmp(clist[k], alist[j]) == 0)
                  break;

            if (k == ncl)
               return FALSE;
         }

         if (j == nand)
            return strcmp(p, param) == 0;
      }

   return 0;
}

/*-------------------------------------------------------------------*/

int param_compare(const void *p1, const void *p2)
{
   return stricmp(((CONFIG_PARAM *) p1)->uparam, ((CONFIG_PARAM *) p2)->uparam);
}

/*------------------------------------------------------------------*/

void free_config()
{
   int i, j;

   for (i = 0; i < n_lb_config; i++) {
      for (j = 0; j < lb_config[i].n_params; j++) {
         xfree(lb_config[i].config_param[j].param);
         xfree(lb_config[i].config_param[j].uparam);
         xfree(lb_config[i].config_param[j].value);
      }
      if (lb_config[i].config_param)
         xfree(lb_config[i].config_param);
      xfree(lb_config[i].section_name);
   }
   xfree(lb_config);
   lb_config = NULL;
   n_lb_config = 0;
}

/*------------------------------------------------------------------*/

int parse_config_file(char *file_name)
/* parse whole config file and store options in sorted list */
{
   char *str, *buffer, *p, *pstr;
   int index, i, j, fh, length;

   str = xmalloc(20000);

   /* open configuration file */
   fh = open(file_name, O_RDONLY | O_BINARY);
   if (fh < 0)
      return 0;
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   buffer = xmalloc(length + 1);
   read(fh, buffer, length);
   buffer[length] = 0;
   close(fh);

   /* release previously allocated memory */
   if (lb_config)
      free_config();

   /* search group */
   p = buffer;
   index = 0;
   do {
      if (*p == '[') {
         p++;
         pstr = str;
         while (*p && *p != ']' && *p != '\n' && *p != '\r' && (int) pstr - (int) str < 10000)
            *pstr++ = *p++;
         *pstr = 0;

         /* allocate new group */
         if (!lb_config)
            lb_config = xmalloc(sizeof(LB_CONFIG));
         else
            lb_config = xrealloc(lb_config, sizeof(LB_CONFIG) * (n_lb_config + 1));
         lb_config[n_lb_config].section_name = xmalloc(strlen(str) + 1);
         lb_config[n_lb_config].n_params = 0;
         lb_config[n_lb_config].config_param = NULL;
         strcpy(lb_config[n_lb_config].section_name, str);

         /* enumerate parameters */
         i = 0;
         p = strchr(p, '\n');
         if (p)
            p++;
         while (p && *p && *p != '[') {
            pstr = str;
            while (*p && *p != '=' && *p != '\n' && *p != '\r' && (int) pstr - (int) str < 10000)
               *pstr++ = *p++;
            *pstr-- = 0;
            while (pstr > str && (*pstr == ' ' || *pstr == '\t' || *pstr == '='))
               *pstr-- = 0;

            if (*p == '=') {

               if (lb_config[n_lb_config].n_params == 0)
                  lb_config[n_lb_config].config_param = xmalloc(sizeof(CONFIG_PARAM));
               else
                  lb_config[n_lb_config].config_param = xrealloc(lb_config[n_lb_config].config_param,
                                                                 sizeof(CONFIG_PARAM) *
                                                                 (lb_config[n_lb_config].n_params + 1));
               lb_config[n_lb_config].config_param[i].param = xmalloc(strlen(str) + 1);
               lb_config[n_lb_config].config_param[i].uparam = xmalloc(strlen(str) + 1);

               strcpy(lb_config[n_lb_config].config_param[i].param, str);
               for (j = 0; j < (int) strlen(str); j++)
                  lb_config[n_lb_config].config_param[i].uparam[j] = toupper(str[j]);
               lb_config[n_lb_config].config_param[i].uparam[j] = 0;

               p++;
               while (*p == ' ' || *p == '\t')
                  p++;
               pstr = str;
               while (*p && *p != '\n' && *p != '\r' && (int) pstr - (int) str < 10000)
                  *pstr++ = *p++;
               *pstr-- = 0;
               while (*pstr == ' ' || *pstr == '\t')
                  *pstr-- = 0;

               lb_config[n_lb_config].config_param[i].value = xmalloc(strlen(str) + 1);
               strcpy(lb_config[n_lb_config].config_param[i].value, str);

               i++;
               lb_config[n_lb_config].n_params = i;
            }

            /* search for next line beginning */
            while (*p && *p != '\r' && *p != '\n')
               p++;
            while (*p && (*p == '\r' || *p == '\n'))
               p++;
         }

         /* sort parameter */
         qsort(lb_config[n_lb_config].config_param, lb_config[n_lb_config].n_params, sizeof(CONFIG_PARAM),
               param_compare);

         n_lb_config++;
         index++;
      }

      /* search for next line beginning */
      while (*p && *p != '\r' && *p != '\n' && *p != '[')
         p++;
      while (*p && (*p == '\r' || *p == '\n'))
         p++;

   } while (*p);

   xfree(str);
   xfree(buffer);
   return 0;
}

/*-------------------------------------------------------------------*/

int getcfg_simple(char *group, char *param, char *value, int vsize, int conditional)
{
   int i, j, status;
   char uparam[256];

   if (strlen(param) >= sizeof(uparam))
      return 0;

   for (i = 0; i < (int) strlen(param); i++)
      uparam[i] = toupper(param[i]);
   uparam[i] = 0;
   value[0] = 0;

   for (i = 0; i < n_lb_config; i++)
      if (strieq(lb_config[i].section_name, group))
         break;

   if (i == n_lb_config)
      return 0;

   for (j = 0; j < lb_config[i].n_params; j++)
      if (match_param(lb_config[i].config_param[j].uparam, uparam, conditional)) {
         status = strchr(lb_config[i].config_param[j].uparam, '{') ? 2 : 1;
         strlcpy(value, lb_config[i].config_param[j].value, vsize);
         return status;
      }

   return 0;
}

/*-------------------------------------------------------------------*/

int enumgrp(int index, char *group)
{
   if (index < n_lb_config) {
      strcpy(group, lb_config[index].section_name);
      return 1;
   }

   return 0;
}

/*-------------------------------------------------------------------*/

int getcfg(char *group, char *param, char *value, int vsize)
/*
   Read parameter from configuration file.

   - if group == [global] and top group exists, read
     from [global <top group>]

   - if parameter not in [global <top group>], read from [global]

   - if group is logbook, read from logbook section

   - if parameter not in [<logbook>], read from [global <top group>]
     or [global]
*/
{
   char str[256];
   int status;

   /* if group is [global] and top group exists, read from there */
   if (strieq(group, "global") && getcfg_topgroup()) {
      sprintf(str, "global %s", getcfg_topgroup());
      if (getcfg(str, param, value, vsize))
         return 1;
   }

   /* first check if parameter is under condition */
   if (_condition[0]) {
      status = getcfg_simple(group, param, value, vsize, TRUE);
      if (status)
         return status;
   }

   status = getcfg_simple(group, param, value, vsize, FALSE);
   if (status)
      return status;

   /* if parameter not found in logbook, look in [global] section */
   if (!group || is_logbook(group))
      return getcfg("global", param, value, vsize);

   return 0;
}

/*-------------------------------------------------------------------*/

char *find_param(char *buf, char *group, char *param)
{
   char *str, *p, *pstr, *pstart;

   /* search group */
   str = xmalloc(10000);
   p = buf;
   do {
      if (*p == '[') {
         p++;
         pstr = str;
         while (*p && *p != ']' && *p != '\n')
            *pstr++ = *p++;
         *pstr = 0;
         if (strieq(str, group)) {
            /* search parameter */
            p = strchr(p, '\n');
            if (p)
               p++;
            while (p && *p && *p != '[') {
               pstr = str;
               pstart = p;
               while (*p && *p != '=' && *p != '\n')
                  *pstr++ = *p++;
               *pstr-- = 0;
               while (pstr > str && (*pstr == ' ' || *pstr == '=' || *pstr == '\t'))
                  *pstr-- = 0;

               if (match_param(str, param, FALSE)) {
                  xfree(str);
                  return pstart;
               }

               if (p)
                  p = strchr(p, '\n');
               if (p)
                  p++;
            }
         }
      }
      if (p)
         p = strchr(p, '\n');
      if (p)
         p++;
   } while (p);

   xfree(str);

   /* now search if in [global] section */
   if (!strieq(group, "global"))
      return find_param(buf, "global", param);

   return NULL;
}

/*-------------------------------------------------------------------*/

int is_group(char *group)
{
   int i;

   for (i = 0; i < n_lb_config; i++)
      if (strieq(group, lb_config[i].section_name))
         return 1;
   return 0;
}

/*------------------------------------------------------------------*/

int enumcfg(char *group, char *param, int psize, char *value, int vsize, int index)
{
   int i;

   for (i = 0; i < n_lb_config; i++)
      if (strieq(group, lb_config[i].section_name)) {

         if (index < lb_config[i].n_params) {
            strlcpy(param, lb_config[i].config_param[index].param, psize);
            strlcpy(value, lb_config[i].config_param[index].value, vsize);
            return 1;
         }

         return 0;

      }

   return 0;
}

/*-------------------------------------------------------------------*/

int exist_top_group()
{
   int i;
   char str[256];

   for (i = 0;; i++) {
      if (!enumcfg("global", str, sizeof(str), NULL, 0, i))
         break;
      str[9] = 0;
      if (strieq(str, "top group"))
         return 1;
   }

   return 0;
}

/*-------------------------------------------------------------------*/

char *_locbuffer = NULL;
char **_porig, **_ptrans;
time_t _locfile_mtime = 0;

/* check if language file changed and if so reload it */
int check_language()
{
   char language[256], file_name[256], *p;
   int fh, length, n;
   struct stat cfg_stat;

   getcfg("global", "Language", language, sizeof(language));

   /* set locale for strftime */
   if (language[0])
      setlocale(LC_ALL, language);
   else
      setlocale(LC_ALL, "english");

   /* force re-read configuration file if changed */
   strlcpy(file_name, resource_dir, sizeof(file_name));
   strlcat(file_name, "eloglang.", sizeof(file_name));
   strlcat(file_name, language, sizeof(file_name));

   if (stat(file_name, &cfg_stat) == 0) {
      if (_locfile_mtime != cfg_stat.st_mtime) {
         _locfile_mtime = cfg_stat.st_mtime;

         if (_locbuffer) {
            xfree(_locbuffer);
            _locbuffer = NULL;
         }
      }
   }

   if (strieq(language, "english") || language[0] == 0) {
      if (_locbuffer) {
         xfree(_locbuffer);
         _locbuffer = NULL;
      }
   } else {
      if (_locbuffer == NULL) {
         fh = open(file_name, O_RDONLY | O_BINARY);
         if (fh < 0)
            return -1;

         length = lseek(fh, 0, SEEK_END);
         lseek(fh, 0, SEEK_SET);
         _locbuffer = xmalloc(length + 1);
         read(fh, _locbuffer, length);
         _locbuffer[length] = 0;
         close(fh);

         /* scan lines, setup orig-translated pointers */
         p = _locbuffer;
         n = 0;
         do {
            while (*p && (*p == '\r' || *p == '\n'))
               p++;

            if (*p && (*p == ';' || *p == '#' || *p == ' ' || *p == '\t')) {
               while (*p && *p != '\n' && *p != '\r')
                  p++;
               continue;
            }

            if (n == 0) {
               _porig = xmalloc(sizeof(char *) * 2);
               _ptrans = xmalloc(sizeof(char *) * 2);
            } else {
               _porig = xrealloc(_porig, sizeof(char *) * (n + 2));
               _ptrans = xrealloc(_ptrans, sizeof(char *) * (n + 2));
            }

            _porig[n] = p;
            while (*p && (*p != '=' && *p != '\r' && *p != '\n'))
               p++;

            if (*p && *p != '=')
               continue;

            _ptrans[n] = p + 1;
            while (*_ptrans[n] == ' ' || *_ptrans[n] == '\t')
               _ptrans[n]++;

            /* remove '=' and surrounding blanks */
            while (*p == '=' || *p == ' ' || *p == '\t')
               *p-- = 0;

            p = _ptrans[n];
            while (*p && *p != '\n' && *p != '\r')
               p++;

            if (p)
               *p++ = 0;

            n++;
         } while (p && *p);

         _porig[n] = NULL;
         _ptrans[n] = NULL;
      }
   }

   return 0;
}

/*-------------------------------------------------------------------*/

/* localization support */
char *loc(char *orig)
{
   int n;
   char language[256];
   static char result[256];

   if (!_locbuffer)
      return orig;

   /* search string and return translation */
   for (n = 0; _porig[n]; n++)
      if (strcmp(orig, _porig[n]) == 0) {
         if (*_ptrans[n])
            return _ptrans[n];
         return orig;
      }

   /* special case: "Change %s" */
   if (strstr(orig, "Change ") && strcmp(orig, "Change %s") != 0) {
      sprintf(result, loc("Change %s"), orig + 7);
      return result;
   }

   /* special case: some intrinsic commands */
   if (strstr(orig, "GetPwdFile")) {
      strcpy(result, orig);
      return result;
   }

   getcfg("global", "Language", language, sizeof(language));
   eprintf("Language error: string \"%s\" not found for language \"%s\"\n", orig, language);

   return orig;
}

/*-------------------------------------------------------------------*/

/* translate back from localized string to english */

char *unloc(char *orig)
{
   int n;

   if (!_locbuffer)
      return orig;

   /* search string and return translation */
   for (n = 0; _ptrans[n]; n++)
      if (strcmp(orig, _ptrans[n]) == 0) {
         if (*_porig[n])
            return _porig[n];
         return orig;
      }

   eprintf("Language error: string \"%s\" not found in English\n", orig);

   return orig;
}

/*-------------------------------------------------------------------*/

char *month_name(int m)
/* return name of month in current locale, m=0..11 */
{
   struct tm ts;
   static char name[32];

   memset(&ts, 0, sizeof(ts));
   ts.tm_mon = m;
   ts.tm_mday = 15;
   ts.tm_year = 2000;

   mktime(&ts);
   strftime(name, sizeof(name), "%B", &ts);
   return name;
}

/*-------------------------------------------------------------------*/

time_t date_to_ltime(char *date)
{
   struct tm tms;
   int i;

   memset(&tms, 0, sizeof(struct tm));

   for (i = 0; i < 12; i++)
      if (strncmp(date + 4, mname[i], 3) == 0)
         break;
   tms.tm_mon = i;

   tms.tm_mday = atoi(date + 8);
   tms.tm_hour = atoi(date + 11);
   tms.tm_min = atoi(date + 14);
   tms.tm_sec = atoi(date + 17);
   tms.tm_year = atoi(date + 20) - 1900;
   tms.tm_isdst = -1;

   if (tms.tm_year < 90)
      tms.tm_year += 100;

   return mktime(&tms);
}

/*-------------------------------------------------------------------*/

void check_config()
{
   check_config_file(FALSE);
   check_language();
}

/*-------------------------------------------------------------------*/

void retrieve_email_from(LOGBOOK * lbs, char *ret, char attrib[MAX_N_ATTR][NAME_LENGTH])
{
   char str[256], *p, login_name[256];
   char slist[MAX_N_ATTR + 10][NAME_LENGTH], svalue[MAX_N_ATTR + 10][NAME_LENGTH];
   int i;

   if (!getcfg(lbs->name, "Use Email from", str, sizeof(str))) {
      if (isparam("user_email") && *getparam("user_email"))
         strcpy(str, getparam("user_email"));
      else
         sprintf(str, "ELog@%s", host_name);
   }

   if (attrib) {
      i = build_subst_list(lbs, slist, svalue, attrib, TRUE);
      strsubst(str, sizeof(str), slist, svalue, i);

      /* remove possible 'mailto:' */
      if ((p = strstr(str, "mailto:")) != NULL)
         strcpy(p, p + 7);
   }

   /* if nothing available, figure out email from an administrator */
   if (strchr(str, '@') == NULL) {
      for (i = 0;; i++) {
         if (!enum_user_line(lbs, i, login_name, sizeof(login_name)))
            break;
         get_user_line(lbs, login_name, NULL, NULL, str, NULL, NULL);
         if (is_admin_user(lbs->name, login_name) && strchr(str, '@'))
            break;
      }
   }

   strcpy(ret, str);
}

/*------------------------------------------------------------------*/

void el_decode(char *message, char *key, char *result)
{
   char *pc, *ph;

   if (result == NULL)
      return;

   *result = 0;

   ph = strstr(message, "========================================");
   if (ph == NULL)
      return;

   do {
      if (ph[40] == '\r' || ph[40] == '\n')
         break;
      ph = strstr(ph + 40, "========================================");
      if (ph == NULL)
         return;

   } while (1);

   /* go through all lines */
   for (pc = message; pc < ph;) {

      if (strncmp(pc, key, strlen(key)) == 0) {
         pc += strlen(key);
         while (*pc != '\n' && *pc != '\r')
            *result++ = *pc++;
         *result = 0;
         return;
      }

      pc = strchr(pc, '\n');
      if (pc == NULL)
         return;
      while (*pc && (*pc == '\n' || *pc == '\r'))
         pc++;
   }
}

/*------------------------------------------------------------------*/

void el_enum_attr(char *message, int n, char *attr_name, char *attr_value)
{
   char *p, str[NAME_LENGTH], tmp[NAME_LENGTH];
   int i;

   p = message;
   for (i = 0; i <= n; i++) {
      strlcpy(str, p, sizeof(str));
      if (strchr(str, '\n'))
         *strchr(str, '\n') = 0;
      if (strchr(str, '\r'))
         *strchr(str, '\r') = 0;

      if (strcmp(str, "========================================") == 0)
         break;

      p = strchr(p, '\n');
      if (!p) {
         str[0] = 0;            /* not a valid line */
         break;
      }
      while (*p == '\n' || *p == '\r')
         p++;

      if (strchr(str, ':')) {
         strcpy(tmp, str);
         *strchr(tmp, ':') = 0;

         if (strieq(tmp, "$@MID@$") ||
             strieq(tmp, "Date") ||
             strieq(tmp, "Attachment") ||
             strieq(tmp, "Reply To") ||
             strieq(tmp, "In Reply To") || strieq(tmp, "Encoding") || strieq(tmp, "Locked by"))
            i--;
      }
   }

   attr_name[0] = 0;
   attr_value[0] = 0;
   if (strchr(str, ':')) {
      strlcpy(attr_name, str, NAME_LENGTH);
      *strchr(attr_name, ':') = 0;
      strlcpy(attr_value, strchr(str, ':') + 2, NAME_LENGTH);
   }

}

/*------------------------------------------------------------------*/

/* Simplified copy of fnmatch() for Cygwin where fnmatch is not defined */

#define EOS '\0'

int fnmatch1(const char *pattern, const char *string)
{
   const char *stringstart;
   char c, test;

   for (stringstart = string;;)
      switch (c = *pattern++) {
      case EOS:
         return (*string == EOS ? 0 : 1);
      case '?':
         if (*string == EOS)
            return (1);
         ++string;
         break;
      case '*':
         c = *pattern;
         /* Collapse multiple stars. */
         while (c == '*')
            c = *++pattern;

         /* Optimize for pattern with * at end or before /. */
         if (c == EOS)
            return (0);

         /* General case, use recursion. */
         while ((test = *string) != EOS) {
            if (!fnmatch1(pattern, string))
               return (0);
            ++string;
         }
         return (1);
         /* FALLTHROUGH */
      default:
         if (c != *string)
            return (1);
         string++;
         break;
      }
}

/*------------------------------------------------------------------*/

INT ss_file_find(char *path, char *pattern, char **plist)
/********************************************************************\

 Routine: ss_file_find

  Purpose: Return list of files matching 'pattern' from the 'path' location

   Input:
   char  *path             Name of a file in file system to check
   char  *pattern          pattern string (wildcard allowed)

    Output:
    char  **plist           pointer to the file list

     Function value:
     int                     Number of files matching request

      \********************************************************************/
{
#ifdef OS_UNIX
   DIR *dir_pointer;
   struct dirent *dp;
   int i;

   if ((dir_pointer = opendir(path)) == NULL)
      return 0;
   *plist = (char *) xmalloc(MAX_PATH_LENGTH);
   i = 0;
   for (dp = readdir(dir_pointer); dp != NULL; dp = readdir(dir_pointer)) {
      if (fnmatch1(pattern, dp->d_name) == 0) {
         *plist = (char *) xrealloc(*plist, (i + 1) * MAX_PATH_LENGTH);
         strncpy(*plist + (i * MAX_PATH_LENGTH), dp->d_name, strlen(dp->d_name));
         *(*plist + (i * MAX_PATH_LENGTH) + strlen(dp->d_name)) = '\0';
         i++;
         seekdir(dir_pointer, telldir(dir_pointer));
      }
   }
   closedir(dir_pointer);
   return i;
#endif

#ifdef OS_WINNT
   HANDLE pffile;
   LPWIN32_FIND_DATA lpfdata;
   char str[255];
   int i, first;

   strlcpy(str, path, sizeof(str));
   strlcat(str, "\\", sizeof(str));
   strlcat(str, pattern, sizeof(str));
   first = 1;
   i = 0;
   lpfdata = xmalloc(sizeof(WIN32_FIND_DATA));
   *plist = (char *) xmalloc(MAX_PATH_LENGTH);
   pffile = FindFirstFile(str, lpfdata);
   if (pffile == INVALID_HANDLE_VALUE)
      return 0;
   first = 0;
   *plist = (char *) xrealloc(*plist, (i + 1) * MAX_PATH_LENGTH);
   strncpy(*plist + (i * MAX_PATH_LENGTH), lpfdata->cFileName, strlen(lpfdata->cFileName));
   *(*plist + (i * MAX_PATH_LENGTH) + strlen(lpfdata->cFileName)) = '\0';
   i++;
   while (FindNextFile(pffile, lpfdata)) {
      *plist = (char *) xrealloc(*plist, (i + 1) * MAX_PATH_LENGTH);
      strncpy(*plist + (i * MAX_PATH_LENGTH), lpfdata->cFileName, strlen(lpfdata->cFileName));
      *(*plist + (i * MAX_PATH_LENGTH) + strlen(lpfdata->cFileName)) = '\0';
      i++;
   }
   xfree(lpfdata);
   return i;
#endif
}

/*------------------------------------------------------------------*/

int eli_compare(const void *e1, const void *e2)
{

   if (((EL_INDEX *) e1)->file_time < ((EL_INDEX *) e2)->file_time)
      return -1;
   if (((EL_INDEX *) e1)->file_time >= ((EL_INDEX *) e2)->file_time)
      return 1;
   return 0;
}

/*------------------------------------------------------------------*/

int el_build_index(LOGBOOK * lbs, BOOL rebuild)
/* scan all ??????a.log files and build an index table in eli[] */
{
   char *file_list, str[256], date[256], dir[256], file_name[MAX_PATH_LENGTH], *buffer,
       *p, *pn, in_reply_to[80];
   int index, n, length;
   int i, fh, len;

   if (rebuild) {
      xfree(lbs->el_index);
      xfree(lbs->n_el_index);
   }

   /* check if this logbook has same data dir as previous */
   for (i = 0; lb_list[i].name[0] && &lb_list[i] != lbs; i++)
      if (strieq(lb_list[i].data_dir, lbs->data_dir))
         break;

   if (strieq(lb_list[i].data_dir, lbs->data_dir)
       && &lb_list[i] != lbs) {
      if (verbose)
         eprintf("\n  Same index as logbook %s\n", lb_list[i].name);

      lbs->el_index = lb_list[i].el_index;
      lbs->n_el_index = lb_list[i].n_el_index;
      return EL_SUCCESS;
   }

   lbs->n_el_index = xmalloc(sizeof(int));
   *lbs->n_el_index = 0;
   lbs->el_index = xmalloc(0);

   /* get data directory */
   strcpy(dir, lbs->data_dir);

   file_list = NULL;
   n = ss_file_find(dir, "??????a.log", &file_list);
   if (n == 0) {
      if (file_list)
         xfree(file_list);
      file_list = NULL;

      n = ss_file_find(dir, "??????.log", &file_list);
      if (n > 0)
         return EL_UPGRADE;

      return EL_EMPTY;
   }

   /* go through all files */
   for (index = 0; index < n; index++) {
      strlcpy(file_name, dir, sizeof(file_name));
      strlcat(file_name, file_list + index * MAX_PATH_LENGTH, sizeof(file_name));

      fh = open(file_name, O_RDWR | O_BINARY, 0644);

      if (fh < 0) {
         sprintf(str, "Cannot open file \"%s\"", file_name);
         eprintf("%s; %s\n", str, strerror(errno));
         return EL_FILE_ERROR;
      }

      /* read file into buffer */
      length = lseek(fh, 0, SEEK_END);

      if (length > 0) {
         buffer = xmalloc(length + 1);
         lseek(fh, 0, SEEK_SET);
         read(fh, buffer, length);
         buffer[length] = 0;
         close(fh);

         /* go through buffer */
         p = buffer;

         do {
            p = strstr(p, "$@MID@$:");

            if (p) {
               lbs->el_index = xrealloc(lbs->el_index, sizeof(EL_INDEX) * (*lbs->n_el_index + 1));
               if (lbs->el_index == NULL) {
                  eprintf("Not enough memory to allocate entry index\n");
                  return EL_MEM_ERROR;
               }

               strcpy(str, file_list + index * MAX_PATH_LENGTH);
               strcpy(lbs->el_index[*lbs->n_el_index].file_name, str);

               el_decode(p, "Date: ", date);
               el_decode(p, "In reply to: ", in_reply_to);

               lbs->el_index[*lbs->n_el_index].file_time = date_to_ltime(date);

               lbs->el_index[*lbs->n_el_index].message_id = atoi(p + 8);
               lbs->el_index[*lbs->n_el_index].offset = (int) p - (int) buffer;
               lbs->el_index[*lbs->n_el_index].in_reply_to = atoi(in_reply_to);

               pn = strstr(p + 8, "$@MID@$:");
               if (pn)
                  len = (int) pn - (int) p;
               else
                  len = strlen(p);

               MD5_checksum(p, len, lbs->el_index[*lbs->n_el_index].md5_digest);

               if (lbs->el_index[*lbs->n_el_index].message_id > 0) {
                  if (verbose) {
                     if (*lbs->n_el_index == 0)
                        eprintf("\n");

                     eprintf("  ID %3d, %s, ofs %5d, %s, MD5=",
                             lbs->el_index[*lbs->n_el_index].message_id,
                             str, lbs->el_index[*lbs->n_el_index].offset,
                             lbs->el_index[*lbs->n_el_index].in_reply_to ? "reply" : "thead");

                     for (i = 0; i < 16; i++)
                        eprintf("%02X", lbs->el_index[*lbs->n_el_index].md5_digest[i]);
                     eprintf("\n");
                  }

                  /* valid ID */
                  (*lbs->n_el_index)++;
               }

               p += 8;
            }

         } while (p);

         xfree(buffer);
      }

   }

   xfree(file_list);

   /* sort entries according to date */
   qsort(lbs->el_index, *lbs->n_el_index, sizeof(EL_INDEX), eli_compare);

   if (verbose) {
      eprintf("After sort:\n");
      for (i = 0; i < *lbs->n_el_index; i++)
         eprintf("  ID %3d, %s, ofs %5d\n", lbs->el_index[i].message_id,
                 lbs->el_index[i].file_name, lbs->el_index[i].offset);
   }

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

int el_index_logbooks()
{
   char str[256], data_dir[256], logbook[256], cwd[256], *p;
   int i, j, n, status;

   if (lb_list) {
      for (i = 0; lb_list[i].name[0]; i++) {
         if (lb_list[i].el_index != NULL) {
            xfree(lb_list[i].el_index);
            xfree(lb_list[i].n_el_index);

            /* check if other logbook uses same index */
            for (j = i + 1; lb_list[j].name[0]; j++) {
               /* mark that logbook already freed */
               if (lb_list[j].el_index == lb_list[i].el_index)
                  lb_list[j].el_index = NULL;
            }
         }
      }
      xfree(lb_list);
   }

   /* count logbooks */
   for (i = n = 0;; i++) {
      if (!enumgrp(i, str))
         break;

      if (!is_logbook(str))
         continue;

      n++;
   }

   lb_list = xcalloc(sizeof(LOGBOOK), n + 1);
   for (i = n = 0;; i++) {
      if (!enumgrp(i, logbook))
         break;

      if (!is_logbook(logbook))
         continue;

      /* check for duplicate name */
      for (j = 0; j < i && lb_list[j].name[0]; j++)
         if (strieq(lb_list[j].name, logbook)) {
            eprintf("Error in configuration file: Duplicate logbook \"%s\"\n", logbook);
            return EL_DUPLICATE;
         }

      /* store logbook in list */
      strcpy(lb_list[n].name, logbook);
      strcpy(lb_list[n].name_enc, logbook);
      url_encode(lb_list[n].name_enc, sizeof(lb_list[n].name_enc));

      /* get data dir from configuration file (old method) */
      if (getcfg(logbook, "Data dir", str, sizeof(str))) {
         if (str[0] == DIR_SEPARATOR || str[1] == ':')
            strlcpy(data_dir, str, sizeof(data_dir));
         else {
            strlcpy(data_dir, resource_dir, sizeof(data_dir));
            strlcat(data_dir, str, sizeof(data_dir));
         }
      } else {
         /* use logbook_dir + "Subdir" (new method) */
         strlcpy(data_dir, logbook_dir, sizeof(data_dir));
         if (data_dir[strlen(data_dir) - 1] != DIR_SEPARATOR)
            strlcat(data_dir, DIR_SEPARATOR_STR, sizeof(data_dir));

         if (getcfg(logbook, "Subdir", str, sizeof(str))) {
            if (str[0] == DIR_SEPARATOR)
               strlcpy(data_dir, str, sizeof(data_dir));
            else
               strlcat(data_dir, str, sizeof(data_dir));
         } else
            strlcat(data_dir, logbook, sizeof(data_dir));       /* use logbook name as default */
      }

      if (data_dir[strlen(data_dir) - 1] != DIR_SEPARATOR)
         strlcat(data_dir, DIR_SEPARATOR_STR, sizeof(data_dir));

      /* create data directory if not existing */
      getcwd(cwd, sizeof(cwd));

      j = chdir(data_dir);
      if (j < 0) {
         p = data_dir;
         if (*p == DIR_SEPARATOR) {
            chdir(DIR_SEPARATOR_STR);
            p++;
         }
         if (p[1] == ':') {
            strcpy(str, p);
            if (str[2] == DIR_SEPARATOR)
               str[3] = 0;
            else
               str[2] = 0;

            chdir(str);
            p += strlen(str);
         }

         do {
            if (strchr(p, DIR_SEPARATOR)) {
               strlcpy(str, p, sizeof(str));
               *strchr(str, DIR_SEPARATOR) = 0;
               p = strchr(p, DIR_SEPARATOR) + 1;
            } else {
               strlcpy(str, p, sizeof(str));
               p = NULL;
            }

            j = chdir(str);
            if (j < 0) {

#ifdef OS_WINNT
               j = mkdir(str);
#else
               j = mkdir(str, 0755);
#endif

               if (j == 0) {
                  if (verbose)
                     eprintf("Created directory \"%s\"\n", str);
               } else {
                  eprintf("el_index_logbooks: %s\n", strerror(errno));
                  eprintf("Cannot create directory \"%s\"\n", str);
               }

               chdir(str);
            }

         } while (p && *p);
      }

      chdir(cwd);
      strcpy(lb_list[n].data_dir, data_dir);
      lb_list[n].el_index = NULL;

      if (verbose)
         eprintf("Indexing logbook \"%s\" ... ", logbook);
      eflush();
      status = el_build_index(&lb_list[n], FALSE);
      if (verbose)
         if (status == EL_SUCCESS)
            eprintf("ok\n");

      if (status == EL_EMPTY) {
         if (verbose)
            eprintf("Found empty logbook \"%s\"\n", logbook);
      } else if (status == EL_UPGRADE) {
         eprintf("Please upgrade data files in \"%s\" with the elconv program.\n", data_dir);
         return EL_UPGRADE;
      } else if (status != EL_SUCCESS) {
         eprintf("Error generating index.\n");
         return status;
      }

      n++;
   }

   /* if top groups defined, set top group in logbook */
   if (exist_top_group()) {
      LBLIST phier;

      phier = get_logbook_hierarchy();
      for (i = 0; i < phier->n_members; i++)
         if (phier->member[i]->is_top)
            for (j = 0; lb_list[j].name[0]; j++)
               if (is_logbook_in_group(phier->member[i], lb_list[j].name))
                  strcpy(lb_list[j].top_group, phier->member[i]->name);


      free_logbook_hierarchy(phier);
   }

   if (!load_password_files())
      return EL_INVAL_FILE;

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

int el_search_message(LOGBOOK * lbs, int mode, int message_id, BOOL head_only)
/********************************************************************\

 Routine: el_search_message

  Purpose: Search for a specific message in a logbook

   Input:
   int   mode              Search mode, EL_FIRST, EL_LAST, EL_NEXT, EL_PREV
   int   message_id        Message id for EL_NEXT and EL_PREV

   Function value:
   int                     New message id

\********************************************************************/
{
   int i;

   if (lbs->n_el_index == 0)
      return 0;

   if (mode == EL_FIRST) {
      if (head_only) {
         for (i = 0; i < *lbs->n_el_index; i++)
            if (lbs->el_index[i].in_reply_to == 0)
               return lbs->el_index[i].message_id;

         return 0;
      }
      if (*lbs->n_el_index == 0)
         return 0;
      return lbs->el_index[0].message_id;
   }

   if (mode == EL_LAST) {
      if (head_only) {
         for (i = *lbs->n_el_index - 1; i >= 0; i--)
            if (lbs->el_index[i].in_reply_to == 0)
               return lbs->el_index[i].message_id;

         return 0;
      }
      if (*lbs->n_el_index == 0)
         return 0;
      return lbs->el_index[*lbs->n_el_index - 1].message_id;
   }

   if (mode == EL_NEXT) {
      for (i = 0; i < *lbs->n_el_index; i++)
         if (lbs->el_index[i].message_id == message_id)
            break;

      if (i == *lbs->n_el_index)
         return 0;              // message not found

      if (i == *lbs->n_el_index - 1)
         return 0;              // last message

      if (head_only) {
         for (i++; i < *lbs->n_el_index; i++)
            if (lbs->el_index[i].in_reply_to == 0)
               return lbs->el_index[i].message_id;

         return 0;
      }

      return lbs->el_index[i + 1].message_id;
   }

   if (mode == EL_PREV) {
      for (i = 0; i < *lbs->n_el_index; i++)
         if (lbs->el_index[i].message_id == message_id)
            break;

      if (i == *lbs->n_el_index)
         return 0;              // message not found

      if (i == 0)
         return 0;              // first message

      if (head_only) {
         for (i--; i >= 0; i--)
            if (lbs->el_index[i].in_reply_to == 0)
               return lbs->el_index[i].message_id;

         return 0;
      }

      return lbs->el_index[i - 1].message_id;
   }

   return 0;
}

/*------------------------------------------------------------------*/

INT el_retrieve(LOGBOOK * lbs,
                int message_id, char *date,
                char attr_list[MAX_N_ATTR][NAME_LENGTH],
                char attrib[MAX_N_ATTR][NAME_LENGTH], int n_attr,
                char *text, int *textsize, char *in_reply_to,
                char *reply_to, char attachment[MAX_ATTACHMENTS][MAX_PATH_LENGTH],
                char *encoding, char *locked_by)
/********************************************************************\

   Routine: el_retrieve

   Purpose: Retrieve an ELog entry by its message tab

   Input:
   LOGBOOK lbs             Logbook structure
   int    message_id       Message ID to retrieve
   int    *size            Size of text buffer

   Output:
   char   *tag             tag of retrieved message
   char   *date            Date/time of message recording
   char   attr_list        Names of attributes
   char   attrib           Values of attributes
   int    n_attr           Number of attributes
   char   *text            Message text
   char   *in_reply_to     Original message if this one is a reply
   char   *reply_to        Replies for current message
   char   *attachment[]    File attachments
   char   *encoding        Encoding of message
   char   *locked_by       User/Host if locked for editing
   int    *size            Actual message text size

   Function value:
   EL_SUCCESS              Successful completion
   EL_EMPTY                Logbook is empty
   EL_NO_MSG               Message doesn't exist
   EL_FILE_ERROR           Internal error

\********************************************************************/
{
   int i, index, size, fh;
   char str[NAME_LENGTH], file_name[256], *p;
   char *message, attachment_all[64 * MAX_ATTACHMENTS];

   if (message_id == 0)
      /* open most recent message */
      message_id = el_search_message(lbs, EL_LAST, 0, FALSE);

   if (message_id == 0)
      return EL_EMPTY;

   for (index = 0; index < *lbs->n_el_index; index++)
      if (lbs->el_index[index].message_id == message_id)
         break;

   if (index == *lbs->n_el_index)
      return EL_NO_MSG;

   sprintf(file_name, "%s%s", lbs->data_dir, lbs->el_index[index].file_name);
   fh = open(file_name, O_RDWR | O_BINARY, 0644);
   if (fh < 0) {
      /* file might have been deleted, rebuild index */
      el_build_index(lbs, TRUE);
      return el_retrieve(lbs, message_id, date, attr_list, attrib, n_attr,
                         text, textsize, in_reply_to, reply_to, attachment, encoding, locked_by);
   }

   message = malloc(TEXT_SIZE + 1000);

   lseek(fh, lbs->el_index[index].offset, SEEK_SET);
   i = read(fh, message, TEXT_SIZE + 1000 - 1);
   if (i <= 0) {
      free(message);
      close(fh);
      return EL_FILE_ERROR;
   }

   message[i] = 0;
   close(fh);

   if (strncmp(message, "$@MID@$:", 8) != 0) {
      free(message);
      /* file might have been edited, rebuild index */
      el_build_index(lbs, TRUE);
      return el_retrieve(lbs, message_id, date, attr_list, attrib, n_attr,
                         text, textsize, in_reply_to, reply_to, attachment, encoding, locked_by);
   }

   /* check for correct ID */
   if (atoi(message + 8) != message_id) {
      free(message);
      return EL_FILE_ERROR;
   }

   /* decode message size */
   p = strstr(message + 8, "$@MID@$:");
   if (p == NULL)
      size = strlen(message);
   else
      size = (int) p - (int) message;

   message[size] = 0;

   /* decode message */
   if (date)
      el_decode(message, "Date: ", date);
   if (reply_to)
      el_decode(message, "Reply to: ", reply_to);
   if (in_reply_to)
      el_decode(message, "In reply to: ", in_reply_to);

   if (n_attr == -1) {
      /* derive attribute names from message */
      for (i = 0;; i++) {
         el_enum_attr(message, i, attr_list[i], attrib[i]);
         if (!attr_list[i][0])
            break;
      }
      n_attr = i;

   } else {
      if (attrib)
         for (i = 0; i < n_attr; i++) {
            sprintf(str, "%s: ", attr_list[i]);
            el_decode(message, str, attrib[i]);
         }
   }

   el_decode(message, "Attachment: ", attachment_all);
   if (encoding)
      el_decode(message, "Encoding: ", encoding);

   if (attachment) {
      /* break apart attachements */
      for (i = 0; i < MAX_ATTACHMENTS; i++)
         if (attachment[i] != NULL)
            attachment[i][0] = 0;

      for (i = 0; i < MAX_ATTACHMENTS; i++) {
         if (attachment[i] != NULL) {
            if (i == 0)
               p = strtok(attachment_all, ",");
            else
               p = strtok(NULL, ",");

            if (p != NULL)
               strcpy(attachment[i], p);
            else
               break;
         }
      }
   }

   if (locked_by)
      el_decode(message, "Locked by: ", locked_by);

   p = strstr(message, "========================================\n");

   /* check for \n -> \r conversion (e.g. zipping/unzipping) */
   if (p == NULL)
      p = strstr(message, "========================================\r");

   if (text) {
      if (p != NULL) {
         p += 41;
         if ((int) strlen(p) >= *textsize) {
            strlcpy(text, p, *textsize);
            show_error("Entry too long to display. Please increase TEXT_SIZE and recompile elogd.");
            free(message);
            return EL_FILE_ERROR;
         } else {
            strlcpy(text, p, *textsize);

            /* strip CR at end */
            if (text[strlen(text) - 1] == '\n') {
               text[strlen(text) - 1] = 0;
               if (text[strlen(text) - 1] == '\r')
                  text[strlen(text) - 1] = 0;
            }

            *textsize = strlen(text);
         }
      } else {
         text[0] = 0;
         *textsize = 0;
      }
   }

   free(message);
   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

int el_submit_attachment(LOGBOOK * lbs, char *afilename, char *buffer, int buffer_size, char *full_name)
{
   char file_name[MAX_PATH_LENGTH], ext_file_name[MAX_PATH_LENGTH + 100], str[MAX_PATH_LENGTH], *p;
   int fh;
   time_t now;
   struct tm tms;

   /* strip directory, add date and time to filename */
   strlcpy(str, afilename, sizeof(str));
   p = str;
   while (strchr(p, ':'))
      p = strchr(p, ':') + 1;
   while (strchr(p, '\\'))
      p = strchr(p, '\\') + 1;  /* NT */
   while (strchr(p, '/'))
      p = strchr(p, '/') + 1;   /* Unix */
   strlcpy(file_name, p, sizeof(file_name));

   /* assemble ELog filename */
   if (file_name[0]) {

      if (file_name[6] == '_' && file_name[13] == '_' && isdigit(file_name[0])
          && isdigit(file_name[1]))
         strlcpy(ext_file_name, file_name, sizeof(ext_file_name));
      else {
         time(&now);
         memcpy(&tms, localtime(&now), sizeof(struct tm));

         sprintf(ext_file_name, "%02d%02d%02d_%02d%02d%02d_%s",
                 tms.tm_year % 100, tms.tm_mon + 1, tms.tm_mday, tms.tm_hour, tms.tm_min,
                 tms.tm_sec, file_name);
      }

      if (full_name)
         strlcpy(full_name, ext_file_name, MAX_PATH_LENGTH);

      strlcpy(str, lbs->data_dir, sizeof(str));
      strlcat(str, ext_file_name, sizeof(str));

      /* save attachment */
      fh = open(str, O_CREAT | O_RDWR | O_BINARY, 0644);
      if (fh < 0) {
         strlcpy(file_name, str, sizeof(str) - 40);
         sprintf(str, "Cannot write attachment file \"%s\"", file_name);
         show_error(str);
         return -1;
      } else {
         write(fh, buffer, buffer_size);
         close(fh);
      }
   }

   return 0;
}

/*------------------------------------------------------------------*/

void el_delete_attachment(LOGBOOK * lbs, char *file_name)
{
   char str[MAX_PATH_LENGTH];

   strlcpy(str, lbs->data_dir, sizeof(str));
   strlcat(str, file_name, sizeof(str));
   remove(str);
   strlcat(str, ".thumb", sizeof(str));
   remove(str);
}

/*------------------------------------------------------------------*/

INT el_retrieve_attachment(LOGBOOK * lbs, int message_id, int n, char name[MAX_PATH_LENGTH])
{
   int i, index, size, fh;
   char file_name[256], *p;
   char message[TEXT_SIZE + 1000], attachment_all[64 * MAX_ATTACHMENTS];

   if (message_id == 0)
      return EL_EMPTY;

   for (index = 0; index < *lbs->n_el_index; index++)
      if (lbs->el_index[index].message_id == message_id)
         break;

   if (index == *lbs->n_el_index)
      return EL_NO_MSG;

   sprintf(file_name, "%s%s", lbs->data_dir, lbs->el_index[index].file_name);
   fh = open(file_name, O_RDWR | O_BINARY, 0644);
   if (fh < 0) {
      /* file might have been deleted, rebuild index */
      el_build_index(lbs, TRUE);
      return el_retrieve_attachment(lbs, message_id, n, name);
   }

   lseek(fh, lbs->el_index[index].offset, SEEK_SET);
   i = read(fh, message, sizeof(message) - 1);
   if (i <= 0) {
      close(fh);
      return EL_FILE_ERROR;
   }

   message[i] = 0;
   close(fh);

   if (strncmp(message, "$@MID@$:", 8) != 0) {
      /* file might have been edited, rebuild index */
      el_build_index(lbs, TRUE);
      return el_retrieve_attachment(lbs, message_id, n, name);
   }

   /* check for correct ID */
   if (atoi(message + 8) != message_id)
      return EL_FILE_ERROR;

   /* decode message size */
   p = strstr(message + 8, "$@MID@$:");
   if (p == NULL)
      size = strlen(message);
   else
      size = (int) p - (int) message;

   message[size] = 0;

   el_decode(message, "Attachment: ", attachment_all);

   name[0] = 0;

   for (i = 0; i <= n; i++) {
      if (i == 0)
         p = strtok(attachment_all, ",");
      else
         p = strtok(NULL, ",");

      if (p == NULL)
         break;
   }

   if (p)
      strlcpy(name, p, MAX_PATH_LENGTH);

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

int el_submit(LOGBOOK * lbs, int message_id, BOOL bedit,
              char *date,
              char attr_name[MAX_N_ATTR][NAME_LENGTH],
              char attr_value[MAX_N_ATTR][NAME_LENGTH],
              int n_attr, char *text,
              char *in_reply_to, char *reply_to,
              char *encoding, char afilename[MAX_ATTACHMENTS][256], BOOL mark_original, char *locked_by)
/********************************************************************\

   Routine: el_submit

   Purpose: Submit an ELog entry

   Input:
   LOGBOOK lbs             Logbook structure
   int    message_id       Message id
   BOOL   bedit            TRUE for existing message, FALSE for new message
   char   *date            Message date
   char   attr_name[][]    Name of attributes
   char   attr_value[][]   Value of attributes
   int    n_attr           Number of attributes

   char   *text            Message text
   char   *in_reply_to     In reply to this message
   char   *reply_to        Replie(s) to this message
   char   *encoding        Text encoding, either HTML or plain

   char   *afilename[]     File name of attachments
   char   *tag             If given, edit existing message
   INT    *tag_size        Maximum size of tag
   BOOL   mark_original    Tag original message for replies
   char   *locked_by       User/Host which locked message for edit

   Function value:
   int                     New message ID

\********************************************************************/
{
   INT n, i, j, size, fh, index, tail_size, orig_size, delta, reply_id;
   char file_name[256], dir[256], str[NAME_LENGTH];
   time_t now, ltime;
   char *message, *p, *buffer;
   char attachment_all[64 * MAX_ATTACHMENTS];

   tail_size = orig_size = 0;

   buffer = NULL;
   message = xmalloc(TEXT_SIZE + 100);

   /* generate new file name YYMMDD.log in data directory */
   strcpy(dir, lbs->data_dir);

   if (bedit) {
      /* edit existing message */
      for (index = 0; index < *lbs->n_el_index; index++)
         if (lbs->el_index[index].message_id == message_id)
            break;

      if (index == *lbs->n_el_index) {
         xfree(message);
         return -1;
      }

      sprintf(file_name, "%s%s", lbs->data_dir, lbs->el_index[index].file_name);
      fh = open(file_name, O_CREAT | O_RDWR | O_BINARY, 0644);
      if (fh < 0) {
         xfree(message);
         return -1;
      }

      lseek(fh, lbs->el_index[index].offset, SEEK_SET);
      i = read(fh, message, TEXT_SIZE + 100);
      message[i] = 0;

      /* check for valid message */
      if (strncmp(message, "$@MID@$:", 8) != 0) {
         close(fh);
         xfree(message);

         /* file might have been edited, rebuild index */
         el_build_index(lbs, TRUE);
         return el_submit(lbs, message_id, bedit, date, attr_name, attr_value,
                          n_attr, text, in_reply_to, reply_to, encoding, afilename, mark_original, locked_by);
      }

      /* check for correct ID */
      if (atoi(message + 8) != message_id) {
         close(fh);
         xfree(message);
         return -1;
      }

      /* decode message size */
      p = strstr(message + 8, "$@MID@$:");
      if (p == NULL)
         size = strlen(message);
      else
         size = (int) p - (int) message;

      message[size] = 0;

      if (strieq(date, "<keep>"))
         el_decode(message, "Date: ", date);
      if (strieq(reply_to, "<keep>"))
         el_decode(message, "Reply to: ", reply_to);
      if (strieq(in_reply_to, "<keep>"))
         el_decode(message, "In reply to: ", in_reply_to);
      el_decode(message, "Attachment: ", attachment_all);

      /* buffer tail of logfile */
      lseek(fh, 0, SEEK_END);
      orig_size = size;
      tail_size = TELL(fh) - (lbs->el_index[index].offset + size);

      if (tail_size > 0) {
         buffer = xmalloc(tail_size);

         lseek(fh, lbs->el_index[index].offset + size, SEEK_SET);
         n = read(fh, buffer, tail_size);
      }
      lseek(fh, lbs->el_index[index].offset, SEEK_SET);
   } else {
      /* create new message */
      if (!date[0]) {
         time(&now);
         strcpy(date, ctime(&now));
         date[24] = 0;
      }

      for (i = 0; i < 12; i++)
         if (strncmp(date + 4, mname[i], 3) == 0)
            break;

      ltime = date_to_ltime(date);

      if (date[8] == ' ')
         date[8] = '0';

      sprintf(file_name, "%c%c%02d%c%ca.log", date[22], date[23], i + 1, date[8], date[9]);

      sprintf(str, "%s%s", dir, file_name);
      fh = open(str, O_CREAT | O_RDWR | O_BINARY, 0644);
      if (fh < 0) {
         xfree(message);
         return -1;
      }

      lseek(fh, 0, SEEK_END);

      /* new message id is old plus one */
      if (message_id == 0) {
         message_id = 1;
         for (i = 0; i < *lbs->n_el_index; i++)
            if (lbs->el_index[i].message_id >= message_id)
               message_id = lbs->el_index[i].message_id + 1;
      }

      /* enter message in index */
      index = *lbs->n_el_index;

      (*lbs->n_el_index)++;
      lbs->el_index = xrealloc(lbs->el_index, sizeof(EL_INDEX) * (*lbs->n_el_index));
      lbs->el_index[index].message_id = message_id;
      strcpy(lbs->el_index[index].file_name, file_name);
      lbs->el_index[index].file_time = ltime;
      lbs->el_index[index].offset = TELL(fh);
      lbs->el_index[index].in_reply_to = atoi(in_reply_to);

      /* if index not ordered, sort it */
      i = *lbs->n_el_index;
      if (i > 1 && lbs->el_index[i - 1].file_time < lbs->el_index[i - 2].file_time) {
         qsort(lbs->el_index, i, sizeof(EL_INDEX), eli_compare);

         /* search message again, index could have been changed by sorting */
         for (index = 0; index < *lbs->n_el_index; index++)
            if (lbs->el_index[index].message_id == message_id)
               break;
      }

      /* if other logbook has same index, update pointers */
      for (i = 0; lb_list[i].name[0]; i++)
         if (&lb_list[i] != lbs && lb_list[i].n_el_index == lbs->n_el_index)
            lb_list[i].el_index = lbs->el_index;
   }

   /* compose message */

   sprintf(message, "$@MID@$: %d\n", message_id);
   sprintf(message + strlen(message), "Date: %s\n", date);

   if (reply_to[0])
      sprintf(message + strlen(message), "Reply to: %s\n", reply_to);

   if (in_reply_to[0])
      sprintf(message + strlen(message), "In reply to: %s\n", in_reply_to);

   for (i = 0; i < n_attr; i++)
      sprintf(message + strlen(message), "%s: %s\n", attr_name[i], attr_value[i]);

   sprintf(message + strlen(message), "Attachment: ");

   if (afilename) {
      sprintf(message + strlen(message), afilename[0]);
      for (i = 1; i < MAX_ATTACHMENTS; i++)
         if (afilename[i][0])
            sprintf(message + strlen(message), ",%s", afilename[i]);
   }
   sprintf(message + strlen(message), "\n");

   sprintf(message + strlen(message), "Encoding: %s\n", encoding);
   if (locked_by && locked_by[0])
      sprintf(message + strlen(message), "Locked by: %s\n", locked_by);

   sprintf(message + strlen(message), "========================================\n");
   strlcat(message, text, TEXT_SIZE + 100);
   strlcat(message, "\n", TEXT_SIZE + 100);

   n = write(fh, message, strlen(message));
   if (n != (int) strlen(message)) {
      if (tail_size > 0)
         xfree(buffer);
      close(fh);
      return -1;
   }

   /* update MD5 checksum */
   MD5_checksum(message, strlen(message), lbs->el_index[index].md5_digest);

   if (bedit) {
      if (tail_size > 0) {
         n = write(fh, buffer, tail_size);
         xfree(buffer);

         /* correct offsets for remaining messages in same file */
         delta = strlen(message) - orig_size;

         for (i = 0; i < *lbs->n_el_index; i++)
            if (lbs->el_index[i].message_id == message_id)
               break;

         for (j = i + 1;
              j < *lbs->n_el_index && strieq(lbs->el_index[i].file_name, lbs->el_index[j].file_name); j++)
            lbs->el_index[j].offset += delta;
      }

      /* truncate file here */
      TRUNCATE(fh);
   }

   close(fh);

   /* if reply, mark original message */
   if (mark_original && in_reply_to[0] && !bedit && atoi(in_reply_to) > 0) {
      char date[80], attr[MAX_N_ATTR][NAME_LENGTH], enc[80], att[MAX_ATTACHMENTS][256],
          reply_to[MAX_REPLY_TO * 10], lock[256];

      reply_id = atoi(in_reply_to);

      /* retrieve original message */
      size = TEXT_SIZE + 100;
      el_retrieve(lbs, reply_id, date, attr_list, attr, n_attr, message, &size,
                  in_reply_to, reply_to, att, enc, lock);

      if (reply_to[0])
         strcat(reply_to, ", ");
      sprintf(reply_to + strlen(reply_to), "%d", message_id);

      /* write modified message */
      el_submit(lbs, reply_id, TRUE, date, attr_list, attr, n_attr, message, in_reply_to,
                reply_to, enc, att, TRUE, lock);
   }

   xfree(message);
   return message_id;
}

/*------------------------------------------------------------------*/

void remove_reference(LOGBOOK * lbs, int message_id, int remove_id, BOOL reply_to_flag)
{
   char date[80], attr[MAX_N_ATTR][NAME_LENGTH], enc[80], in_reply_to[80],
       reply_to[MAX_REPLY_TO * 10], att[MAX_ATTACHMENTS][256], lock[256], *p, *ps, *message;
   int size, status;

   /* retrieve original message */
   size = TEXT_SIZE + 1000;
   message = xmalloc(size);
   status =
       el_retrieve(lbs, message_id, date, attr_list, attr, lbs->n_attr, message, &size,
                   in_reply_to, reply_to, att, enc, lock);
   if (status != EL_SUCCESS)
      return;

   if (reply_to_flag)
      p = reply_to;
   else
      p = in_reply_to;

   while (*p) {
      while (*p && (*p == ',' || *p == ' '))
         p++;

      ps = p;
      while (isdigit(*ps))
         ps++;

      while (*ps && (*ps == ',' || *ps == ' '))
         ps++;

      if (atoi(p) == remove_id)
         strcpy(p, ps);
      else
         while (isdigit(*p))
            p++;
   }

   /* write modified message */
   el_submit(lbs, message_id, TRUE, date, attr_list, attr, lbs->n_attr, message,
             in_reply_to, reply_to, enc, att, TRUE, lock);

   xfree(message);
}

/*------------------------------------------------------------------*/

INT el_delete_message(LOGBOOK * lbs, int message_id,
                      BOOL delete_attachments,
                      char attachment[MAX_ATTACHMENTS][MAX_PATH_LENGTH],
                      BOOL delete_bw_ref, BOOL delete_reply_to)
/********************************************************************\

   Routine: el_delete_message

   Purpose: Delete an ELog entry including attachments

   Input:
   LOGBOOK *lbs          Pointer to logbook structure
   int     message_id    Message ID
   BOOL    delete_attachments   Delete attachments if TRUE
   char    attachment    Used to return attachments (on move)
   BOOL    delete_bw_ref If true, delete backward references
   BOOL    delete_reply_to If true, delete replies to this message

   Output:
   <none>

   Function value:
   EL_SUCCESS              Successful completion

\********************************************************************/
{
   INT i, index, n, size, fh, tail_size, old_offset;
   char str[MAX_PATH_LENGTH], file_name[MAX_PATH_LENGTH], reply_to[MAX_REPLY_TO * 10], in_reply_to[256];
   char *buffer, *p;
   char *message, attachment_all[64 * MAX_ATTACHMENTS];

   for (index = 0; index < *lbs->n_el_index; index++)
      if (lbs->el_index[index].message_id == message_id)
         break;

   if (index == *lbs->n_el_index)
      return -1;

   sprintf(file_name, "%s%s", lbs->data_dir, lbs->el_index[index].file_name);
   fh = open(file_name, O_RDWR | O_BINARY, 0644);
   if (fh < 0)
      return EL_FILE_ERROR;

   message = xmalloc(TEXT_SIZE + 1000);

   lseek(fh, lbs->el_index[index].offset, SEEK_SET);
   i = read(fh, message, TEXT_SIZE + 1000 - 1);
   if (i <= 0) {
      xfree(message);
      close(fh);
      return EL_FILE_ERROR;
   }

   if (_logging_level > 1)
      write_logfile(lbs, "DELETE entry #%d", message_id);

   message[i] = 0;

   if (strncmp(message, "$@MID@$:", 8) != 0) {
      close(fh);
      xfree(message);

      /* file might have been edited, rebuild index */
      el_build_index(lbs, TRUE);
      return el_delete_message(lbs, message_id, delete_attachments, attachment,
                               delete_bw_ref, delete_reply_to);
   }

   /* check for correct ID */
   if (atoi(message + 8) != message_id) {
      close(fh);
      xfree(message);
      return EL_FILE_ERROR;
   }

   /* decode message size */
   p = strstr(message + 8, "$@MID@$:");
   if (p == NULL)
      size = strlen(message);
   else
      size = (int) p - (int) message;

   message[size] = 0;

   /* delete attachments */
   el_decode(message, "Attachment: ", attachment_all);

   for (i = 0; i < MAX_ATTACHMENTS; i++) {
      if (i == 0)
         p = strtok(attachment_all, ",");
      else
         p = strtok(NULL, ",");

      if (attachment != NULL) {
         if (attachment[i][0] && p) {
            /* delete old attachment if new one exists */
            el_delete_attachment(lbs, p);
         }

         /* return old attachment if no new one */
         if (!attachment[i][0] && p)
            strcpy(attachment[i], p);
      }

      if (delete_attachments && p)
         el_delete_attachment(lbs, p);
   }

   /* decode references */
   el_decode(message, "Reply to: ", reply_to);
   el_decode(message, "In reply to: ", in_reply_to);

   /* buffer tail of logfile */
   lseek(fh, 0, SEEK_END);
   tail_size = TELL(fh) - (lbs->el_index[index].offset + size);

   buffer = NULL;
   if (tail_size > 0) {
      buffer = xmalloc(tail_size);

      lseek(fh, lbs->el_index[index].offset + size, SEEK_SET);
      n = read(fh, buffer, tail_size);
   }
   lseek(fh, lbs->el_index[index].offset, SEEK_SET);

   if (tail_size > 0) {
      n = write(fh, buffer, tail_size);
      xfree(buffer);
   }

   /* truncate file here */
   TRUNCATE(fh);

   /* if file length gets zero, delete file */
   tail_size = lseek(fh, 0, SEEK_END);
   close(fh);
   xfree(message);

   if (tail_size == 0)
      remove(file_name);

   /* remove message from index */
   strcpy(str, lbs->el_index[index].file_name);
   old_offset = lbs->el_index[index].offset;
   for (i = index; i < *lbs->n_el_index - 1; i++)
      memcpy(&lbs->el_index[i], &lbs->el_index[i + 1], sizeof(EL_INDEX));

   (*lbs->n_el_index)--;
   if (*lbs->n_el_index > 0)
      lbs->el_index = xrealloc(lbs->el_index, sizeof(EL_INDEX) * (*lbs->n_el_index));

   /* correct all offsets after deleted message */
   for (i = 0; i < *lbs->n_el_index; i++)
      if (strieq(lbs->el_index[i].file_name, str) && lbs->el_index[i].offset > old_offset)
         lbs->el_index[i].offset -= size;

   /* if other logbook has same index, update pointers */
   for (i = 0; lb_list[i].name[0]; i++)
      if (&lb_list[i] != lbs && lb_list[i].n_el_index == lbs->n_el_index)
         lb_list[i].el_index = lbs->el_index;

   /* delete also replies to this message */
   if (delete_reply_to && reply_to[0]) {
      p = reply_to;
      if (isdigit(*p))
         do {
            if (atoi(p))
               el_delete_message(lbs, atoi(p), TRUE, NULL, FALSE, TRUE);

            while (*p && isdigit(*p))
               p++;
            while (*p && (*p == ',' || *p == ' '))
               p++;
         } while (*p);
   }

   /* delete backward references */
   if (in_reply_to[0] && delete_bw_ref) {
      p = in_reply_to;
      do {
         if (atoi(p))
            remove_reference(lbs, atoi(p), message_id, TRUE);

         while (*p && isdigit(*p))
            p++;
         while (*p && (*p == ',' || *p == ' '))
            p++;
      } while (*p);
   }

   /* execute shell if requested */
   if (getcfg(lbs->name, "Execute delete", str, sizeof(str)))
      execute_shell(lbs, message_id, NULL, NULL, str);

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

int el_correct_links(LOGBOOK * lbs, int old_id, int new_id)
/* If a message gets resubmitted, the links to that message are wrong.
This routine corrects that. */
{
   int i, i1, n, n1, size;
   char date[80], *attrib, *text, in_reply_to[80], reply_to[MAX_REPLY_TO * 10], encoding[80], locked_by[256];
   char list[MAX_N_ATTR][NAME_LENGTH], list1[MAX_N_ATTR][NAME_LENGTH];
   char *att_file;

   attrib = xmalloc(MAX_N_ATTR * NAME_LENGTH);
   text = xmalloc(TEXT_SIZE);
   att_file = xmalloc(MAX_ATTACHMENTS * 256);

   el_retrieve(lbs, new_id, date, attr_list, (void *) attrib, lbs->n_attr, NULL, 0,
               in_reply_to, reply_to, (void *) att_file, encoding, locked_by);

   /* go through in_reply_to list */
   n = strbreak(in_reply_to, list, MAX_N_ATTR, ",");
   for (i = 0; i < n; i++) {
      size = TEXT_SIZE;
      el_retrieve(lbs, atoi(list[i]), date, attr_list, (void *) attrib, lbs->n_attr,
                  text, &size, in_reply_to, reply_to, (void *) att_file, encoding, locked_by);

      n1 = strbreak(reply_to, list1, MAX_N_ATTR, ",");
      reply_to[0] = 0;
      for (i1 = 0; i1 < n1; i1++) {
         /* replace old ID by new ID */
         if (atoi(list1[i1]) == old_id)
            sprintf(reply_to + strlen(reply_to), "%d", new_id);
         else
            strcat(reply_to, list1[i1]);

         if (i1 < n1 - 1)
            strcat(reply_to, ", ");
      }

      el_submit(lbs, atoi(list[i]), TRUE, date, attr_list, (void *) attrib, lbs->n_attr,
                text, in_reply_to, reply_to, encoding, (void *) att_file, TRUE, locked_by);
   }

   el_retrieve(lbs, new_id, date, attr_list, (void *) attrib, lbs->n_attr, NULL, 0,
               in_reply_to, reply_to, (void *) att_file, encoding, locked_by);

   /* go through reply_to list */
   n = strbreak(reply_to, list, MAX_N_ATTR, ",");
   for (i = 0; i < n; i++) {
      size = sizeof(text);
      el_retrieve(lbs, atoi(list[i]), date, attr_list, (void *) attrib, lbs->n_attr,
                  text, &size, in_reply_to, reply_to, (void *) att_file, encoding, locked_by);

      n1 = strbreak(in_reply_to, list1, MAX_N_ATTR, ",");
      in_reply_to[0] = 0;
      for (i1 = 0; i1 < n1; i1++) {
         /* replace old ID by new ID */
         if (atoi(list1[i1]) == old_id)
            sprintf(in_reply_to + strlen(in_reply_to), "%d", new_id);
         else
            strcat(in_reply_to, list1[i1]);

         if (i1 < n1 - 1)
            strcat(in_reply_to, ", ");
      }

      el_submit(lbs, atoi(list[i]), TRUE, date, attr_list, (void *) attrib, lbs->n_attr,
                text, in_reply_to, reply_to, encoding, (void *) att_file, TRUE, locked_by);
   }

   xfree(text);
   xfree(attrib);
   xfree(att_file);

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

int el_move_message_thread(LOGBOOK * lbs, int message_id)
{
   int i, n, size, new_id;
   char date[80], attrib[MAX_N_ATTR][NAME_LENGTH], *text,
       in_reply_to[80], reply_to[MAX_REPLY_TO * 10], encoding[80], locked_by[256];
   char list[MAX_N_ATTR][NAME_LENGTH], str[256];
   char att_file[MAX_ATTACHMENTS][256];

   /* retrieve message */
   text = xmalloc(TEXT_SIZE);
   size = TEXT_SIZE;
   el_retrieve(lbs, message_id, date, attr_list, attrib, lbs->n_attr,
               text, &size, in_reply_to, reply_to, att_file, encoding, locked_by);

   /* submit as new message */
   date[0] = 0;
   new_id = el_submit(lbs, 0, FALSE, date, attr_list, attrib, lbs->n_attr, text,
                      in_reply_to, reply_to, encoding, att_file, FALSE, locked_by);

   xfree(text);

   /* correct links */
   el_correct_links(lbs, message_id, new_id);

   /* delete original message */
   el_delete_message(lbs, message_id, FALSE, NULL, FALSE, FALSE);

   /* move all replies recursively */
   if (getcfg(lbs->name, "Resubmit replies", str, sizeof(str)) && atoi(str) == 1) {
      if (reply_to[0]) {
         n = strbreak(reply_to, list, MAX_N_ATTR, ",");
         for (i = 0; i < n; i++)
            el_move_message_thread(lbs, atoi(list[i]));
      }
   }

   return new_id;
}

/*------------------------------------------------------------------*/

int el_move_message(LOGBOOK * lbs, int old_id, int new_id)
{
   int status, size;
   char date[80], attrib[MAX_N_ATTR][NAME_LENGTH], *text, in_reply_to[80],
       reply_to[MAX_REPLY_TO * 10], encoding[80], locked_by[256], att_file[MAX_ATTACHMENTS][256];

   /* retrieve message */
   text = xmalloc(TEXT_SIZE);
   size = TEXT_SIZE;
   status = el_retrieve(lbs, old_id, date, attr_list, attrib, lbs->n_attr,
                        text, &size, in_reply_to, reply_to, att_file, encoding, locked_by);
   if (status != EL_SUCCESS)
      return 0;

   /* submit as new message */
   status = el_submit(lbs, new_id, FALSE, date, attr_list, attrib, lbs->n_attr, text,
                      in_reply_to, reply_to, encoding, att_file, FALSE, locked_by);

   xfree(text);

   if (status != new_id)
      return 0;

   /* correct links */
   el_correct_links(lbs, old_id, new_id);

   /* delete original message */
   el_delete_message(lbs, old_id, FALSE, NULL, FALSE, FALSE);

   return 1;
}

/*------------------------------------------------------------------*/

int el_lock_message(LOGBOOK * lbs, int message_id, char *user)
/* lock message for editing */
{
   int size;
   char date[80], attrib[MAX_N_ATTR][NAME_LENGTH], text[TEXT_SIZE],
       in_reply_to[80], reply_to[MAX_REPLY_TO * 10], encoding[80], locked_by[256];
   char att_file[MAX_ATTACHMENTS][256];

   /* retrieve message */
   size = sizeof(text);
   el_retrieve(lbs, message_id, date, attr_list, attrib, lbs->n_attr,
               text, &size, in_reply_to, reply_to, att_file, encoding, locked_by);

   /* submit message, unlocked if user==NULL */
   el_submit(lbs, message_id, TRUE, date, attr_list, attrib, lbs->n_attr, text,
             in_reply_to, reply_to, encoding, att_file, FALSE, user);

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

void write_logfile(LOGBOOK * lbs, const char *format, ...)
{
   char file_name[2000];
   va_list argptr;
   char str[10000];
   FILE *f;
   time_t now;
   char buf[10000];

   if (lbs == NULL) {
      if (!getcfg("global", "logfile", str, sizeof(str)))
         return;
   } else if (!getcfg(lbs->name, "logfile", str, sizeof(str)))
      return;

   if (str[0] == DIR_SEPARATOR || str[1] == ':')
      strlcpy(file_name, str, sizeof(file_name));
   else {
      strlcpy(file_name, resource_dir, sizeof(file_name));
      strlcat(file_name, str, sizeof(file_name));
   }

   va_start(argptr, format);
   vsprintf(str, (char *) format, argptr);
   va_end(argptr);

   f = fopen(file_name, "a");
   if (!f)
      return;

   now = time(0);
   strftime(buf, sizeof(buf), "%d-%b-%Y %H:%M:%S", localtime(&now));
   strcat(buf, " ");

   if (*getparam("unm") && rem_host[0])
      sprintf(buf + strlen(buf), "[%s@%s] ", getparam("unm"), rem_host);
   else if (rem_host[0])
      sprintf(buf + strlen(buf), "[%s] ", rem_host);

   if (lbs)
      sprintf(buf + strlen(buf), "{%s} ", lbs->name);

   strlcat(buf, str, sizeof(buf));
   if (buf[strlen(buf) - 1] != '\n')
      strlcat(buf, "\n", sizeof(buf));

   fprintf(f, buf);

   fclose(f);
}

/*------------------------------------------------------------------*/

/*
void logd(const char *format, ...)
{
   va_list argptr;
   char str[10000];
   FILE *f;
   time_t now;
   char buf[1000];

   va_start(argptr, format);
   vsprintf(str, (char *) format, argptr);
   va_end(argptr);

   f = fopen("c:\\tmp\\elogd.log", "a");
   if (!f)
      return;

   now = time(0);
   strftime(buf, sizeof(buf), "%d-%b-%Y %H:%M:%S", localtime(&now));
   strcat(buf, " ");

   strlcat(buf, str, sizeof(buf));
   if (buf[strlen(buf) - 1] != '\n')
      strlcat(buf, "\n", sizeof(buf));

   fprintf(f, buf);

   fclose(f);
}
*/

/*------------------------------------------------------------------*/

char *html_tags[] = { "<A HREF=", "<IMG ", "<B>", "<I>", "<P>", "<HR>", "" };

int is_html(char *s)
{
   char *str, *p;
   int i;

   str = xstrdup(s);

   for (i = 0; i < (int) strlen(s); i++)
      str[i] = toupper(s[i]);
   str[i] = 0;

   for (i=0 ; html_tags[i][0] ; i++) {
      p = strstr(str, html_tags[i]);
      if (p && strchr(p, '>') && (p == str || (p > str && *(p-1) != '\\'))) {
         xfree(str);
         return TRUE;
      }
   }

   xfree(str);
   return FALSE;
}

/*------------------------------------------------------------------*/

int is_ascii(char *file_name)
{
   int i, fh, length;
   unsigned char *buf;

   fh = open(file_name, O_RDONLY | O_BINARY);
   if (fh < 0)
      return FALSE;
   lseek(fh, 0, SEEK_END);
   length = TELL(fh);
   lseek(fh, 0, SEEK_SET);
   if (length > 1000)
      length = 1000;
   buf = xmalloc(length);
   read(fh, buf, length);
   close(fh);

   for (i = 0; i < length; i++) {
      if (buf[i] < 32 && buf[i] != '\r' && buf[i] != '\n' && buf[i] != '\t') {
         xfree(buf);
         return FALSE;
      }
   }

   xfree(buf);
   return TRUE;
}

/*------------------------------------------------------------------*/

int is_image(char *att)
{
   return (stristr(att, ".GIF") != NULL) ||
       (stristr(att, ".JPG") != NULL) || (stristr(att, ".JPEG") != NULL) || (stristr(att, ".PNG") != NULL);
}

/*------------------------------------------------------------------*/

void strip_html(char *s)
{
   char *p;

   while ((p = strchr(s, '<')) != NULL) {
      if (strchr(p, '>'))
         strcpy(p, strchr(p, '>') + 1);
      else
         *p = 0;
   }
}

/*------------------------------------------------------------------*/

void insert_breaks(char *str, int n, int size)
{
   int i, j, i_last;

   i_last = 0;
   for (i = 0; i < (int) strlen(str); i++) {
      if (str[i] == '\r')
         i_last = i;

      /* if more than n chars without return, insert one */
      if (i - i_last >= n && (int) strlen(str) + 3 < size) {

         /* find previous blank */
         while (i > i_last && str[i] != ' ')
            i--;
         if (str[i] == ' ')
            i++;

         /* move trailing string one char further */
         for (j = strlen(str) + 2; j > i; j--)
            str[j] = str[j - 2];

         /* set CR */
         str[i++] = '\r';
         str[i++] = '\n';

         i_last = i;
      }
   }
}

/*------------------------------------------------------------------*/

void rsputs(const char *str)
{
   if (strlen_retbuf + (int) strlen(str) > return_buffer_size) {
      return_buffer = xrealloc(return_buffer, return_buffer_size + 100000);
      memset(return_buffer + return_buffer_size, 0, 100000);
      return_buffer_size += 100000;
   }

   strcpy(return_buffer + strlen_retbuf, str);
   strlen_retbuf += strlen(str);
}

/*------------------------------------------------------------------*/

char *key_list[] = { "http://", "https://", "ftp://", "mailto:", "elog:", "file://", "" };

void rsputs2(LOGBOOK *lbs, const char *str)
{
   int i, j, k, l, m, n;
   char *p, *pd, link[1000], link_text[1000], tmp[1000], base_url[256];

   if (strlen_retbuf + (int) (2 * strlen(str) + 1000) >= return_buffer_size) {
      return_buffer = xrealloc(return_buffer, return_buffer_size + 100000);
      memset(return_buffer + return_buffer_size, 0, 100000);
      return_buffer_size += 100000;
   }

   j = strlen_retbuf;
   for (i = 0; i < (int) strlen(str); i++) {
      for (l = 0; key_list[l][0]; l++) {
         if (strncmp(str + i, key_list[l], strlen(key_list[l])) == 0) {
            p = (char *) (str + i + strlen(key_list[l]));
            i += strlen(key_list[l]);
            for (k = 0; *p && strcspn(p, " \t\n\r({[)}]\"") && k < (int) sizeof(link); k++, i++)
               link[k] = *p++;
            link[k] = 0;
            i--;

            /* link may not end with a '.'/',' (like in a sentence) */
            if (link[k - 1] == '.' || link[k - 1] == ',') {
               link[k - 1] = 0;
               k--;
               i--;
            }

            /* check if link contains coloring */
            p = strchr(link, '\001');
            if (p != NULL) {
               strlcpy(link_text, link, sizeof(link_text));

               /* skip everything between '<' and '>' */
               pd = p;
               while (*pd && *pd != '\002')
                  *p = *pd++;

               strcpy(p, pd + 1);

               /* skip '</B>' */
               p = strchr(link, '\001');
               if (p != NULL) {
                  pd = p;

                  while (*pd && *pd != '\002')
                     *p = *pd++;

                  strcpy(p, pd + 1);
               }

               /* correct link text */
               for (n = 0; n < (int) strlen(link_text); n++) {
                  switch (link_text[n]) {
                     /* the translation for the search highliting */
                  case '\001':
                     link_text[n] = '<';
                     break;
                  case '\002':
                     link_text[n] = '>';
                     break;
                  case '\003':
                     link_text[n] = '\"';
                     break;
                  case '\004':
                     link_text[n] = ' ';
                     break;
                  }
               }

            } else
               strlcpy(link_text, link, sizeof(link_text));

            if (strcmp(key_list[l], "elog:") == 0) {
               strlcpy(tmp, link, sizeof(tmp));
               if (strchr(tmp, '/'))
                  *strchr(tmp, '/') = 0;

               for (m = 0; m < (int) strlen(tmp); m++)
                  if (!isdigit(tmp[m]))
                     break;

               if (m < (int) strlen(tmp)) {
                  /* if link contains reference to other logbook, put logbook explicitly */
                  compose_base_url(NULL, base_url, sizeof(base_url));              
                  sprintf(return_buffer + j, "<a href=\"%s%s\">elog:%s</a>", base_url, link, link_text);
               } else if (link[0] == '/') {
                  compose_base_url(lbs, base_url, sizeof(base_url));              
                  sprintf(return_buffer + j, "<a href=\"%s%d%s\">elog:%s</a>", base_url,
                          _current_message_id, link, link_text);
               } else {
                  compose_base_url(lbs, base_url, sizeof(base_url));              
                  sprintf(return_buffer + j, "<a href=\"%s%s\">elog:%s</a>", base_url, link, link_text);
               }

            } else if (strcmp(key_list[l], "mailto:") == 0) {
               sprintf(return_buffer + j, "<a href=\"mailto:%s\">%s</a>", link, link_text);
            } else {
               sprintf(return_buffer + j, "<a href=\"%s", key_list[l]);
               j += strlen(return_buffer + j);
               strlen_retbuf = j;

               /* link can contain special characters */
               rsputs2(lbs, link);
               j = strlen_retbuf;

               sprintf(return_buffer + j, "\">%s", key_list[l]);
               j += strlen(return_buffer + j);
               strlen_retbuf = j;

               /* link_text can contain special characters */
               rsputs2(lbs, link_text);
               j = strlen_retbuf;
               sprintf(return_buffer + j, "</a>");
            }

            j += strlen(return_buffer + j);
            break;
         }
      }

      if (!key_list[l][0]) {
         if (strncmp(str + i, "<br>", 4) == 0) {
            strcpy(return_buffer + j, "<br>");
            j += 4;
            i += 3;
         } else
            switch (str[i]) {
            case '&':
               strcat(return_buffer, "&amp;");
               j += 5;
               break;
            case '<':
               strcat(return_buffer, "&lt;");
               j += 4;
               break;
            case '>':
               strcat(return_buffer, "&gt;");
               j += 4;
               break;
            
            /* suppress escape character '\' in front of HTML or ELCode tag */  	 
            case '\\': 	 
               if (str[i+1] != '<' && str[i+1] != '[') 	 
                  return_buffer[j++] = str[i]; 	 
               break;

            /* the translation for the search highliting */
            case '\001':
               strcat(return_buffer, "<");
               j++;
               break;
            case '\002':
               strcat(return_buffer, ">");
               j++;
               break;
            case '\003':
               strcat(return_buffer, "\"");
               j++;
               break;
            case '\004':
               strcat(return_buffer, " ");
               j++;
               break;

            default:
               return_buffer[j++] = str[i];
            }
      }
   }

   return_buffer[j] = 0;
   strlen_retbuf = j;
}

/*------------------------------------------------------------------*/

void rsputs3(const char *text)
{
   int i;
   char str[2];

   str[1] = 0;
   for (i = 0; i < (int) strlen(text); i++) {
      switch (text[i]) {
      case '<':
         rsputs("&lt;");
         break;
      case '>':
         rsputs("&gt;");
         break;
      case '&':
         rsputs("&amp;");
         break;
      case '\"':
         rsputs("&quot;");
         break;

      default:
         str[0] = text[i];
         rsputs(str);
      }
   }
}

/*------------------------------------------------------------------*/

typedef struct {
   char *pattern;
   char *subst;
} PATTERN_LIST;

PATTERN_LIST pattern_list[] = {

   /* escape */
   {"\\[", "["},

   /* smileys */
   {":))", "<img src=\"%s/icons/happy.png\">"},
   {":-))", "<img src=\"%sicons/happy.png\">"},
   {":)", "<img src=\"%sicons/smile.png\">"},
   {":-)", "<img src=\"%sicons/smile.png\">"},
   {":(", "<img src=\"%sicons/frown.png\">"},
   {":-(", "<img src=\"%sicons/frown.png\">"},
   {";)", "<img src=\"%sicons/wink.png\">"},
   {";-)", "<img src=\"%sicons/wink.png\">"},
   {":D", "<img src=\"%sicons/biggrin.png\">"},
   {"?)", "<img src=\"%sicons/confused.png\">"},
   {";(", "<img src=\"%sicons/crying.png\">"},
   {";-(", "<img src=\"%sicons/crying.png\">"},
   {":]", "<img src=\"%sicons/pleased.png\">"},
   {":-]", "<img src=\"%sicons/pleased.png\">"},
   {":O", "<img src=\"%sicons/yawn.png\">"},
   {":-O", "<img src=\"%sicons/yawn.png\">"},
   {"8)", "<img src=\"%sicons/cool.png\">"},
   {"8-)", "<img src=\"%sicons/cool.png\">"},
   {"8o", "<img src=\"%sicons/eek.png\">"},
   {"X(", "<img src=\"%sicons/mad.png\">"},
   {":P", "<img src=\"%sicons/tongue.png\">"},
   {":-P", "<img src=\"%sicons/tongue.png\">"},

   /* formatting */
   {"[b]", "<b>"},
   {"[/b]", "</b>"},
   {"[u]", "<u>"},
   {"[/u]", "</u>"},
   {"[i]", "<i>"},
   {"[/i]", "</i>"},

   {"[center]", "<center>"},
   {"[/center]", "</center>"},
   {"[color=", "<font color=\"%s\">"},
   {"[/color]", "</font>"},
   {"[size=", "<font size=\"%s\">"},
   {"[/size]", "</font>"},
   {"[font=", "<font face=\"%s\">"},
   {"[/font]", "</font>"},
   {"\r\n[code]", "<pre>"},
   {"[code]", "<pre>"},
   {"[/code]\r\n", "</pre>"},
   {"[/code]", "</pre>"},

   /* lists */
   {"[list]\r", "<ul>"},
   {"[list]", "<ul>"},
   {"[*]", "<li>"},
   {"[/list]\r", "</ul>"},
   {"[/list]", "</ul>"},
   {"[list=", "<ol type=\"%s\">"},

   /* URLs */
   {"[url=", "<a href=\"%#\">%s</a>"},
   {"[url]", "<a href=\"%#\">%s</a>"},
   {"[/url]", ""},
   {"[email]", "<a href=\"mailto:%#\">%s</a>"},
   {"[/email]", ""},
   {"[img]", "<img src=\"%#\">"},
   {"[/img]", ""},

   /* quote */
   {"[quote=",
    "<br /><table class=\"quotetable\" align=\"center\" cellspacing=\"1\"><tr><td class=\"quotetitle\">%s:</td></tr><tr><td class=\"quote\">"},
   {"[quote]",
    "<br /><table class=\"quotetable\" align=\"center\" cellspacing=\"1\"><tr><td class=\"quotetitle\">%s:</td></tr><tr><td class=\"quote\">"},
   {"[/quote]\r", "</td></tr></table><br />"},
   {"[/quote]", "</td></tr></table>"},

   {"", ""}
};

void rsputs_elcode(LOGBOOK * lbs, const char *str)
{
   int i, j, k, l, m, n, interprete_elcode;
   char *p, *pd, link[1000], link_text[1000], tmp[1000], attrib[1000], hattrib[1000],
       value[1000], subst[1000], base_url[256], param[256];

   if (strlen_retbuf + (int) (2 * strlen(str) + 1000) >= return_buffer_size) {
      return_buffer = xrealloc(return_buffer, return_buffer_size + 100000);
      memset(return_buffer + return_buffer_size, 0, 100000);
      return_buffer_size += 100000;
   }

   interprete_elcode = TRUE;
   j = strlen_retbuf;
   for (i = 0; i < (int) strlen(str); i++) {
      for (l = 0; key_list[l][0]; l++) {
         if (strncmp(str + i, key_list[l], strlen(key_list[l])) == 0) {
            p = (char *) (str + i + strlen(key_list[l]));
            i += strlen(key_list[l]);
            for (k = 0; *p && strcspn(p, " \t\n\r({[)}]\"") && k < (int) sizeof(link); k++, i++)
               link[k] = *p++;
            link[k] = 0;
            i--;

            /* link may not end with a '.'/',' (like in a sentence) */
            if (link[k - 1] == '.' || link[k - 1] == ',') {
               link[k - 1] = 0;
               k--;
               i--;
            }

            /* check if link contains coloring */
            p = strchr(link, '\001');
            if (p != NULL) {
               strlcpy(link_text, link, sizeof(link_text));

               /* skip everything between '<' and '>' */
               pd = p;
               while (*pd && *pd != '\002')
                  *p = *pd++;

               strcpy(p, pd + 1);

               /* skip '</B>' */
               p = strchr(link, '\001');
               if (p != NULL) {
                  pd = p;

                  while (*pd && *pd != '\002')
                     *p = *pd++;

                  strcpy(p, pd + 1);
               }

               /* correct link text */
               for (n = 0; n < (int) strlen(link_text); n++) {
                  switch (link_text[n]) {
                     /* the translation for the search highliting */
                  case '\001':
                     link_text[n] = '<';
                     break;
                  case '\002':
                     link_text[n] = '>';
                     break;
                  case '\003':
                     link_text[n] = '\"';
                     break;
                  case '\004':
                     link_text[n] = ' ';
                     break;
                  }
               }

            } else
               strlcpy(link_text, link, sizeof(link_text));

            if (strcmp(key_list[l], "elog:") == 0) {
               strlcpy(tmp, link, sizeof(tmp));
               if (strchr(tmp, '/'))
                  *strchr(tmp, '/') = 0;

               for (m = 0; m < (int) strlen(tmp); m++)
                  if (!isdigit(tmp[m]))
                     break;

               if (m < (int) strlen(tmp)) {
                  /* if link contains reference to other logbook, put logbook explicitly */
                  compose_base_url(NULL, base_url, sizeof(base_url));              
                  sprintf(return_buffer + j, "<a href=\"%s%s\">elog:%s</a>", base_url, link, link_text);
               } else if (link[0] == '/') {
                  compose_base_url(lbs, base_url, sizeof(base_url));              
                  sprintf(return_buffer + j, "<a href=\"%s%d%s\">elog:%s</a>", base_url,
                          _current_message_id, link, link_text);
               } else {
                  compose_base_url(lbs, base_url, sizeof(base_url));              
                  sprintf(return_buffer + j, "<a href=\"%s%s\">elog:%s</a>", base_url, link, link_text);
               }
            } else if (strcmp(key_list[l], "mailto:") == 0) {
               sprintf(return_buffer + j, "<a href=\"mailto:%s\">%s</a>", link, link_text);
            } else {
               sprintf(return_buffer + j, "<a href=\"%s", key_list[l]);
               j += strlen(return_buffer + j);
               strlen_retbuf = j;

               /* link can contain special characters */
               rsputs2(lbs, link);
               j = strlen_retbuf;

               sprintf(return_buffer + j, "\">%s", key_list[l]);
               j += strlen(return_buffer + j);
               strlen_retbuf = j;

               /* link_text can contain special characters */
               rsputs2(lbs, link_text);
               j = strlen_retbuf;
               sprintf(return_buffer + j, "</a>");
            }

            j += strlen(return_buffer + j);
            break;
         }
      }
      if (key_list[l][0])
         continue;

      for (l = 0; pattern_list[l].pattern[0]; l++) {
         if (strnieq(str + i, pattern_list[l].pattern, strlen(pattern_list[l].pattern))) {

            if (stristr(pattern_list[l].pattern, "[/code]")) 
               interprete_elcode = TRUE;

            if (interprete_elcode) {

               if (stristr(pattern_list[l].pattern, "[quote")) {
                  if (pattern_list[l].pattern[strlen(pattern_list[l].pattern) - 1] == '=') {
                     i += strlen(pattern_list[l].pattern);
                     strextract(str + i, ']', attrib, sizeof(attrib));
                     i += strlen(attrib);

                     if (attrib[0] == '\"')
                        strcpy(attrib, attrib + 1);
                     if (attrib[strlen(attrib) - 1] == '\"')
                        attrib[strlen(attrib) - 1] = 0;

                     sprintf(value, loc("%s wrote"), attrib);
                     sprintf(return_buffer + j, pattern_list[l].subst, value);
                     j += strlen(return_buffer + j);
                  } else {
                     sprintf(return_buffer + j, pattern_list[l].subst, loc("Quote"));
                     j += strlen(return_buffer + j);
                     i += strlen(pattern_list[l].pattern) - 1;     // 1 gets added in for loop...
                  }
               }

               else if (strstr(pattern_list[l].subst, "%#")) {

                  /* special substitutions */
                  if (pattern_list[l].pattern[strlen(pattern_list[l].pattern) - 1] == '=') {

                     i += strlen(pattern_list[l].pattern);
                     strextract(str + i, ']', attrib, sizeof(attrib));
                     i += strlen(attrib) + 1;

                     if (strncmp(attrib, "elog:", 5) == 0) {       /* eval elog: */
                        strlcpy(tmp, attrib + 5, sizeof(tmp));
                        if (strchr(tmp, '/'))
                           *strchr(tmp, '/') = 0;

                        for (m = 0; m < (int) strlen(tmp); m++)
                           if (!isdigit(tmp[m]))
                              break;

                        if (m < (int) strlen(tmp))
                           /* if link contains reference to other logbook, add ".." in front */
                           sprintf(hattrib, "../%s", attrib + 5);
                        else if (attrib[5] == '/')
                           sprintf(hattrib, "%d%s", _current_message_id, attrib + 5);
                        else
                           sprintf(hattrib, "%s", attrib + 5);

                     } else if (strncmp(attrib, "http://", 7) != 0)        /* add http:// if missing */
                        sprintf(hattrib, "http://%s", attrib);
                     else
                        strlcpy(hattrib, attrib, sizeof(hattrib));

                     strextract(str + i, '[', value, sizeof(value));
                     i += strlen(value) - 1;
                     strlcpy(subst, pattern_list[l].subst, sizeof(subst));
                     *strchr(subst, '#') = 's';
                     sprintf(return_buffer + j, subst, hattrib, value);

                     j += strlen(return_buffer + j);

                  } else if (pattern_list[l].pattern[strlen(pattern_list[l].pattern) - 1] != '=') {

                     i += strlen(pattern_list[l].pattern);
                     strextract(str + i, '[', attrib, sizeof(attrib));
                     i += strlen(attrib) - 1;
                     strlcpy(hattrib, attrib, sizeof(hattrib));

                     /* change /x to id/x for images */
                     if (strnieq(attrib, "elog:/", 6)) {
                        compose_base_url(lbs, hattrib, sizeof(hattrib));
                        if (_current_message_id == 0) {
                           sprintf(param, "attachment%d", atoi(attrib + 6)-1);
                           if (isparam(param))
                              strlcat(hattrib, getparam(param), sizeof(hattrib));
                        } else
                        sprintf(hattrib+strlen(hattrib), "%d%s", _current_message_id, attrib + 5);
                     }

                     /* add http:// if missing */
                     else if (!strnieq(attrib, "http://", 7) &&
                              strstr(pattern_list[l].subst, "mailto") == NULL &&
                              strstr(pattern_list[l].subst, "img") == NULL)
                        sprintf(hattrib, "http://%s", attrib);
                     strlcpy(subst, pattern_list[l].subst, sizeof(subst));
                     *strchr(subst, '#') = 's';
                     sprintf(return_buffer + j, subst, hattrib, attrib);
                     j += strlen(return_buffer + j);
                  }

               } else if (pattern_list[l].pattern[strlen(pattern_list[l].pattern) - 1] == '=') {

                  /* extract sting after '=' and put it into '%s' of subst */
                  i += strlen(pattern_list[l].pattern);
                  strextract(str + i, ']', attrib, sizeof(attrib));
                  i += strlen(attrib);
                  sprintf(return_buffer + j, pattern_list[l].subst, attrib);
                  j += strlen(return_buffer + j);

               } else {

                  /* simple substitution */
                  strcpy(link, pattern_list[l].subst);
                  if (strstr(link, "%s")) {
                     strcpy(tmp, link);
                     compose_base_url(lbs, base_url, sizeof(base_url));
                     sprintf(link, tmp, base_url);
                  }

                  strcpy(return_buffer + j, link);
                  j += strlen(link);
                  i += strlen(pattern_list[l].pattern) - 1;        // 1 gets added in for loop...
               }
            } // interprete_elcode

            if (stristr(pattern_list[l].pattern, "[code]")) 
               interprete_elcode = FALSE;

            break;
         }
      }
      if ((interprete_elcode && pattern_list[l].pattern[0]) || stristr(pattern_list[l].pattern, "[code]"))
         continue;

      if (strnieq(str + i, "<br>", 4)) {
         strcpy(return_buffer + j, "<br />");
         j += 6;
         i += 3;
      } else
         switch (str[i]) {
         case '\r':
            if (interprete_elcode) {
               strcat(return_buffer, "<br />\r\n");
               j += 8;
            } else {
               strcat(return_buffer, "\r\n");
               j += 2;
            }
            break;
         case '\n':
            break;
         case '&':
            strcat(return_buffer, "&amp;");
            j += 5;
            break;
         case '<':
            strcat(return_buffer, "&lt;");
            j += 4;
            break;
         case '>':
            strcat(return_buffer, "&gt;");
            j += 4;
            break;

         /* the translation for the search highliting */
         case '\001':
            strcat(return_buffer, "<");
            j++;
            break;
         case '\002':
            strcat(return_buffer, ">");
            j++;
            break;
         case '\003':
            strcat(return_buffer, "\"");
            j++;
            break;
         case '\004':
            strcat(return_buffer, " ");
            j++;
            break;

         default:
            return_buffer[j++] = str[i];
         }
   }

   return_buffer[j] = 0;
   strlen_retbuf = j;
}

/*------------------------------------------------------------------*/

void rsprintf(const char *format, ...)
{
   va_list argptr;
   char str[10000];

   va_start(argptr, format);
   vsprintf(str, (char *) format, argptr);
   va_end(argptr);

   if (strlen_retbuf + (int) strlen(str) > return_buffer_size) {
      return_buffer = xrealloc(return_buffer, return_buffer_size + 100000);
      memset(return_buffer + return_buffer_size, 0, 100000);
      return_buffer_size += 100000;
   }

   strcpy(return_buffer + strlen_retbuf, str);

   strlen_retbuf += strlen(str);
}

/*------------------------------------------------------------------*/

void flush_return_buffer()
{
   send(_sock, return_buffer, strlen_retbuf, 0);
   memset(return_buffer, 0, return_buffer_size);
   strlen_retbuf = 0;
}

/*------------------------------------------------------------------*/

/* Parameter handling functions similar to setenv/getenv */

void initparam()
{
   memset(_param, 0, sizeof(_param));
   memset(_value, 0, sizeof(_value));
   _mtext[0] = 0;
   _cmdline[0] = 0;
}

int setparam(char *param, char *value)
{
   int i;
   char str[10000];

   if (strieq(param, "text")) {
      if (strlen(value) >= TEXT_SIZE) {
         sprintf(str,
                 "Error: Entry text too big (%d bytes). Please increase TEXT_SIZE and recompile elogd\n",
                 strlen(value));
         show_error(str);
         return 0;
      }

      strlcpy(_mtext, value, TEXT_SIZE);
      return 1;
   }

   if (strieq(param, "cmdline")) {
      if (strlen(value) >= CMD_SIZE) {
         sprintf(str,
                 "Error: Command line too big (%d bytes). Please increase CMD_SIZE and recompile elogd\n",
                 strlen(value));
         show_error(str);
         return 0;
      }

      strlcpy(_cmdline, value, CMD_SIZE);
      return 1;
   }

   /* paremeters can be superseeded */
   for (i = 0; i < MAX_PARAM; i++)
      if (_param[i][0] == 0 || strieq(param, _param[i]))
         break;

   if (i < MAX_PARAM) {
      if (strlen(param) >= NAME_LENGTH) {
         sprintf(str, "Error: Parameter name too big (%d bytes).\n", strlen(param));
         show_error(str);
         return 0;
      }

      strlcpy(_param[i], param, NAME_LENGTH);

      if (strlen(value) >= NAME_LENGTH) {
         sprintf(str,
                 "Error: Parameter value for parameter <b>%s</b> too big (%d bytes). Please increase NAME_LENGTH and recompile elogd\n",
                 param, strlen(value));
         show_error(str);
         return 0;
      }

      strlcpy(_value[i], value, NAME_LENGTH);
   } else {
      sprintf(str, "Error: Too many parameters (> %d). Cannot perform operation.\n", MAX_PARAM);
      show_error(str);
      return 0;
   }

   return 1;
}

char *getparam(char *param)
{
   int i;

   if (strieq(param, "text"))
      return _mtext;

   if (strieq(param, "cmdline"))
      return _cmdline;

   for (i = 0; i < MAX_PARAM && _param[i][0]; i++)
      if (strieq(param, _param[i]))
         break;

   if (i < MAX_PARAM)
      return _value[i];

   return NULL;
}

BOOL enumparam(int n, char *param, char *value)
{
   param[0] = value[0] = 0;

   if (n >= MAX_PARAM)
      return FALSE;

   if (_param[n][0] == 0)
      return FALSE;

   strcpy(param, _param[n]);
   strcpy(value, _value[n]);

   return TRUE;
}

BOOL isparam(char *param)
{
   int i;

   for (i = 0; i < MAX_PARAM && _param[i][0]; i++)
      if (strieq(param, _param[i]))
         break;

   if (i < MAX_PARAM && _param[i][0])
      return TRUE;

   return FALSE;
}

void unsetparam(char *param)
{
   int i;

   for (i = 0; i < MAX_PARAM; i++)
      if (strieq(param, _param[i]))
         break;

   if (i < MAX_PARAM) {
      for (; i < MAX_PARAM - 1; i++) {
         strlcpy(_param[i], _param[i + 1], NAME_LENGTH);
         strlcpy(_value[i], _value[i + 1], NAME_LENGTH);
      }
      _param[MAX_PARAM - 1][0] = 0;
      _value[MAX_PARAM - 1][0] = 0;
   }
}

/*------------------------------------------------------------------*/

void extract_path(char *str)
{
   char *p, str2[256];

   p = NULL;

   if (strstr(str, "http://"))
      p = str + 7;
   if (strstr(str, "https://"))
      p = str + 8;

   if (p) {
      while (*p && *p != '/')
         p++;
      if (*p == '/')
         p++;

      strcpy(str2, p);
      strcpy(str, str2);
      if (str[strlen(str) - 1] == '/')
         str[strlen(str) - 1] = 0;
   }
}

/*------------------------------------------------------------------*/

void extract_host(char *str)
{
   char *p, *ph, str2[256];

   p = NULL;

   if (strstr(str, "http://"))
      p = str + 7;
   else if (strstr(str, "https://"))
      p = str + 8;

   if (p) {
      ph = p;
      while (*p && *p != '/' && *p != ':')
         p++;
      *p = 0;

      strcpy(str2, ph);
      strcpy(str, str2);
   }
}

/*------------------------------------------------------------------*/

void compose_base_url(LOGBOOK * lbs, char *base_url, int size)
{
   /* try to get URL from referer */
   if (!getcfg("global", "URL", base_url, size)) {
      if (referer[0] && stristr(referer, "https:"))
         strcpy(base_url, "https://");
      else
         strcpy(base_url, "http://");

      if (elog_tcp_port == 80)
         sprintf(base_url+strlen(base_url), "%s/", host_name);
      else
         sprintf(base_url+strlen(base_url), "%s:%d/", host_name, elog_tcp_port);
      if (lbs) {
         strlcat(base_url, lbs->name_enc, size);
         strlcat(base_url, "/", size);
      }
   } else {
      if (base_url[strlen(base_url) - 1] != '/')
         strlcat(base_url, "/", size);
      if (lbs) {
         strlcat(base_url, lbs->name_enc, size);
         strlcat(base_url, "/", size);
      }
   }
}

/*------------------------------------------------------------------*/

void set_location(LOGBOOK * lbs, char *rel_path)
{
   char str[NAME_LENGTH];

   if (strncmp(rel_path, "http://", 7) == 0) {
      rsputs("Location: ");
      rsputs(rel_path);
   } else if (strncmp(rel_path, "https://", 8) == 0) {
      rsputs("Location: ");
      rsputs(rel_path);
   } else {
      if (lbs)
         getcfg(lbs->name, "URL", str, sizeof(str));
      else
         getcfg("global", "URL", str, sizeof(str));

      /* if HTTP request comes from localhost, use localhost as
         absolute link (needed if running on DSL at home) */
      if (!str[0] && strstr(http_host, "localhost")) {
         strcpy(str, "http://localhost");
         if (elog_tcp_port != 80)
            sprintf(str + strlen(str), ":%d", elog_tcp_port);
         strcat(str, "/");
      }

      if (!str[0]) {
         /* assemble absolute path from host name and port */
         sprintf(str, "http://%s", host_name);
         if (elog_tcp_port != 80)
            sprintf(str + strlen(str), ":%d", elog_tcp_port);
         strcat(str, "/");
      }

      /* add trailing '/' if not present */
      if (str[strlen(str) - 1] != '/')
         strcat(str, "/");

      rsputs("Location: ");
      rsputs(str);

      /* add top group if existing and not logbook */
      if (!lbs && getcfg_topgroup()) {
         rsputs(getcfg_topgroup());
         rsputs("/");
      }

      if (strncmp(rel_path, "../", 3) == 0)
         rsputs(rel_path + 3);
      else if (strcmp(rel_path, ".") == 0) {
         if (lbs)
            rsputs(lbs->name_enc);
      } else if (rel_path[0] == '/')
         rsputs(rel_path + 1);
      else {
         if (lbs) {
            rsputs(lbs->name_enc);
            rsputs("/");
            rsputs(rel_path);
         } else
            rsputs(rel_path);
      }
   }

   rsprintf("\r\n\r\n<html>redir</html>\r\n");
}

/*------------------------------------------------------------------*/

void set_redir(LOGBOOK * lbs, char *redir)
{
   char str[NAME_LENGTH];

   str[0] = 0;

   /* prepare relative path */
   if (redir[0])
      strcpy(str, redir);
   else {
      if (lbs)
         sprintf(str, "../%s/", lbs->name_enc);
      else if (getcfg_topgroup())
         sprintf(str, ".");
   }

   set_location(lbs, str);
}

/*------------------------------------------------------------------*/

void set_cookie(LOGBOOK * lbs, char *name, char *value, BOOL global, char *expiration)
{
   char lb_name[256], str[NAME_LENGTH], format[80];
   double exp;
   time_t now;
   struct tm *gmt;

   if (lbs)
      strcpy(lb_name, lbs->name);
   else
      strcpy(lb_name, "global");

   rsprintf("Set-Cookie: %s=%s;", name, value);

   /* add path */
   if (global) {
      /* path for all logbooks */
      if (getcfg(lb_name, "URL", str, sizeof(str))) {
         extract_path(str);
         url_encode(str, sizeof(str));
         rsprintf(" path=/%s;", str);
      } else
         rsprintf(" path=/;");
   } else {
      /* path for individual logbook */
      if (getcfg(lb_name, "URL", str, sizeof(str))) {
         extract_path(str);
         url_encode(str, sizeof(str));
         if (str[0])
            rsprintf(" path=/%s/%s;", str, lbs->name_enc);
         else
            rsprintf(" path=/%s;", lbs->name_enc);
      } else
         rsprintf(" path=/%s;", lbs->name_enc);
   }

   exp = atof(expiration);

   /* to clear a cookie, set expiration date to yesterday */
   if (value[0] == 0)
      exp = -24;

   /* add expriation date */
   if (exp != 0 && exp < 100000) {
      time(&now);
      now += (int) (3600 * exp);
      gmt = gmtime(&now);
      strcpy(format, "%A, %d-%b-%y %H:%M:%S GMT");
      strftime(str, sizeof(str), format, gmt);

      rsprintf(" expires=%s;", str);
   }

   rsprintf("\r\n");
}

/*------------------------------------------------------------------*/

void redirect(LOGBOOK * lbs, char *rel_path)
{
   /* redirect */
   rsprintf("HTTP/1.1 302 Found\r\n");
   rsprintf("Server: ELOG HTTP %s\r\n", VERSION);
   if (use_keepalive) {
      rsprintf("Connection: Keep-Alive\r\n");
      rsprintf("Keep-Alive: timeout=60, max=10\r\n");
   }

   set_location(lbs, rel_path);
}

/*------------------------------------------------------------------*/

int strbreak(char *str, char list[][NAME_LENGTH], int size, char *brk)
/* break comma-separated list into char array, stripping leading
and trailing blanks */
{
   int i, j;
   char *p;

   memset(list, 0, size * NAME_LENGTH);
   p = str;
   if (!p || !*p)
      return 0;

   while (*p == ' ')
      p++;

   for (i = 0; *p && i < size; i++) {
      if (*p == '"') {
         p++;
         j = 0;
         memset(list[i], 0, NAME_LENGTH);
         do {
            /* convert two '"' to one */
            if (*p == '"' && *(p + 1) == '"') {
               list[i][j++] = '"';
               p += 2;
            } else if (*p == '"') {
               break;
            } else
               list[i][j++] = *p++;

         } while (j < NAME_LENGTH - 1);
         list[i][j] = 0;

         /* skip second '"' */
         p++;

         /* skip blanks and break character */
         while (*p == ' ')
            p++;
         if (*p && strchr(brk, *p))
            p++;
         while (*p == ' ')
            p++;

      } else {
         strlcpy(list[i], p, NAME_LENGTH);

         for (j = 0; j < (int) strlen(list[i]); j++)
            if (strchr(brk, list[i][j]))
               list[i][j] = 0;

         p += strlen(list[i]);
         while (*p == ' ')
            p++;
         if (*p && strchr(brk, *p))
            p++;
         while (*p == ' ')
            p++;
      }

      while (list[i][strlen(list[i]) - 1] == ' ')
         list[i][strlen(list[i]) - 1] = 0;

      if (!*p)
         break;
   }

   if (i == size)
      return size;

   return i + 1;
}

/*------------------------------------------------------------------*/

int scan_attributes(char *logbook)
/* scan configuration file for attributes and fill attr_list, attr_options
and attr_flags arrays */
{
   char list[10000], str[NAME_LENGTH], type[NAME_LENGTH], tmp_list[MAX_N_ATTR][NAME_LENGTH];
   int i, j, n, m;

   if (getcfg(logbook, "Attributes", list, sizeof(list))) {
      /* reset attribute flags */
      memset(attr_flags, 0, sizeof(attr_flags));

      /* get attribute list */
      memset(attr_list, 0, sizeof(attr_list));
      n = strbreak(list, attr_list, MAX_N_ATTR, ",");

      /* get options lists for attributes */
      memset(attr_options, 0, sizeof(attr_options));
      for (i = 0; i < n; i++) {
         sprintf(str, "Options %s", attr_list[i]);
         if (getcfg(logbook, str, list, sizeof(list)))
            strbreak(list, attr_options[i], MAX_N_LIST, ",");

         sprintf(str, "MOptions %s", attr_list[i]);
         if (getcfg(logbook, str, list, sizeof(list))) {
            strbreak(list, attr_options[i], MAX_N_LIST, ",");
            attr_flags[i] |= AF_MULTI;
         }

         sprintf(str, "ROptions %s", attr_list[i]);
         if (getcfg(logbook, str, list, sizeof(list))) {
            strbreak(list, attr_options[i], MAX_N_LIST, ",");
            attr_flags[i] |= AF_RADIO;
         }

         sprintf(str, "IOptions %s", attr_list[i]);
         if (getcfg(logbook, str, list, sizeof(list))) {
            strbreak(list, attr_options[i], MAX_N_LIST, ",");
            attr_flags[i] |= AF_ICON;
         }
      }

      /* check if attribute required */
      getcfg(logbook, "Required Attributes", list, sizeof(list));
      m = strbreak(list, tmp_list, MAX_N_ATTR, ",");
      for (i = 0; i < m; i++) {
         for (j = 0; j < n; j++)
            if (strieq(attr_list[j], tmp_list[i]))
               attr_flags[j] |= AF_REQUIRED;
      }

      /* check if locked attribute */
      getcfg(logbook, "Locked Attributes", list, sizeof(list));
      m = strbreak(list, tmp_list, MAX_N_ATTR, ",");
      for (i = 0; i < m; i++) {
         for (j = 0; j < n; j++)
            if (strieq(attr_list[j], tmp_list[i]))
               attr_flags[j] |= AF_LOCKED;
      }

      /* check if fixed attribute for Edit */
      getcfg(logbook, "Fixed Attributes Edit", list, sizeof(list));
      m = strbreak(list, tmp_list, MAX_N_ATTR, ",");
      for (i = 0; i < m; i++) {
         for (j = 0; j < n; j++)
            if (strieq(attr_list[j], tmp_list[i]))
               attr_flags[j] |= AF_FIXED_EDIT;
      }

      /* check if fixed attribute for Reply */
      getcfg(logbook, "Fixed Attributes Reply", list, sizeof(list));
      m = strbreak(list, tmp_list, MAX_N_ATTR, ",");
      for (i = 0; i < m; i++) {
         for (j = 0; j < n; j++)
            if (strieq(attr_list[j], tmp_list[i]))
               attr_flags[j] |= AF_FIXED_REPLY;
      }

      /* check for extendable options */
      getcfg(logbook, "Extendable Options", list, sizeof(list));
      m = strbreak(list, tmp_list, MAX_N_ATTR, ",");
      for (i = 0; i < m; i++) {
         for (j = 0; j < n; j++)
            if (strieq(attr_list[j], tmp_list[i]))
               attr_flags[j] |= AF_EXTENDABLE;
      }

      for (i = 0; i < n; i++) {
         sprintf(str, "Type %s", attr_list[i]);
         if (getcfg(logbook, str, type, sizeof(type))) {
            if (strieq(type, "date"))
               attr_flags[i] |= AF_DATE;
            if (strieq(type, "datetime"))
               attr_flags[i] |= AF_DATETIME;
            if (strieq(type, "time"))
               attr_flags[i] |= AF_TIME;
            if (strieq(type, "numeric"))
               attr_flags[i] |= AF_NUMERIC;
            if (strieq(type, "userlist"))
               attr_flags[i] |= AF_USERLIST;
         }
      }

   } else {
      memcpy(attr_list, attr_list_default, sizeof(attr_list_default));
      memcpy(attr_options, attr_options_default, sizeof(attr_options_default));
      memcpy(attr_flags, attr_flags_default, sizeof(attr_flags_default));
      n = 4;
   }

   return n;
}

/*------------------------------------------------------------------*/

void show_http_header(LOGBOOK * lbs, BOOL expires, char *cookie)
{
   char str[256];

   rsprintf("HTTP/1.1 200 Document follows\r\n");
   rsprintf("Server: ELOG HTTP %s\r\n", VERSION);

   if (getcfg("global", "charset", str, sizeof(str)))
      rsprintf("Content-Type: text/html;charset=%s\r\n", str);
   else
      rsprintf("Content-Type: text/html;charset=%s\r\n", DEFAULT_HTTP_CHARSET);

   if (cookie && cookie[0]) {
      rsprintf("Set-Cookie: %s;", cookie);

      if (getcfg(lbs->name, "URL", str, sizeof(str))) {
         extract_path(str);
         url_encode(str, sizeof(str));
         if (str[0])
            rsprintf(" path=/%s/%s;", str, lbs->name_enc);
         else
            rsprintf(" path=/%s;", lbs->name_enc);
      } else
         rsprintf(" path=/%s;", lbs->name_enc);
      rsprintf("\r\n");
   }

   if (use_keepalive) {
      rsprintf("Connection: Keep-Alive\r\n");
      rsprintf("Keep-Alive: timeout=60, max=10\r\n");
   }

   if (expires) {
      rsprintf("Pragma: no-cache\r\n");
      rsprintf("Expires: Fri, 01 Jan 1983 00:00:00 GMT\r\n");
   }

   rsprintf("\r\n");
}

void show_plain_header(int size, char *file_name)
{
   /* header */
   rsprintf("HTTP/1.1 200 Document follows\r\n");
   rsprintf("Server: ELOG HTTP %s\r\n", VERSION);
   rsprintf("Accept-Ranges: bytes\r\n");

   if (use_keepalive) {
      rsprintf("Connection: Keep-Alive\r\n");
      rsprintf("Keep-Alive: timeout=60, max=10\r\n");
   }

   rsprintf("Pragma: no-cache\r\n");
   rsprintf("Expires: Fri, 01 Jan 1983 00:00:00 GMT\r\n");
   rsprintf("Content-Type: text/plain\r\n");
   rsprintf("Content-disposition: attachment; filename=\"%s\"\r\n", file_name);
   if (size)
      rsprintf("Content-Length: %d\r\n", size);
   rsprintf("\r\n");
}

void show_html_header(LOGBOOK * lbs, BOOL expires, char *title, BOOL close_head, BOOL rss_feed, char *cookie)
{
   char css[256], str[256];

   show_http_header(lbs, expires, cookie);

   /* DOCTYPE */
   rsprintf("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n");

   /* page title */
   rsprintf("<html><head><title>%s</title>\n", title);

   /* Cascading Style Sheet */
   compose_base_url(lbs, css, sizeof(css));

   if (lbs != NULL && getcfg(lbs->name, "CSS", str, sizeof(str)))
      strlcat(css, str, sizeof(css));
   else if (lbs == NULL && getcfg("global", "CSS", str, sizeof(str)))
      strlcat(css, str, sizeof(css));
   else
      strlcat(css, "default.css", sizeof(css));

   rsprintf("<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\">\n", css);
   rsprintf("<link rel=\"shortcut icon\" href=\"favicon.ico\">\n");
   rsprintf("<link rel=\"icon\" href=\"favicon.png\" type=\"image/png\">\n");

   if (rss_feed) {
      rsprintf("<link rel=\"alternate\" type=\"application/rss+xml\" ");
      rsprintf("title=\"ELOG %s\" ", lbs->name);
      rsprintf("href=\"elog.rdf\">\n");
   }

   if (close_head)
      rsprintf("</head>\n");
}

void show_standard_header(LOGBOOK * lbs, BOOL expires, char *title, char *path, BOOL rss_feed, char *cookie)
{
   show_html_header(lbs, expires, title, TRUE, rss_feed, cookie);

   rsprintf("<body>\n");

   show_top_text(lbs);

   if (path && path[0])
      rsprintf("<form name=form1 method=\"GET\" action=\"%s\">\n\n", path);
   else
      rsprintf("<form name=form1 method=\"GET\" action=\".\">\n\n");
}

/*------------------------------------------------------------------*/

void show_upgrade_page(LOGBOOK * lbs)
{
   char str[1000];

   show_html_header(lbs, FALSE, "ELOG Upgrade Information", TRUE, FALSE, NULL);

   rsprintf("<body>\n");

   rsprintf("<table class=\"frame\" cellpadding=0 cellspacing=0>\n\n");

   rsprintf("<tr><td class=\"title2\">ELog Electronic Logbook Upgrade Information</font></td></tr>\n");

   rsprintf("<tr><td class=\"form1\"><br>\n");

   rsprintf("You probably use an <b>%s</b> configuration file for a ELOG version\n", CFGFILE);
   rsprintf("1.1.x, since it contains a <b><code>\"Types = ...\"</code></b> entry. From version\n");
   rsprintf("1.2.0 on, the fixed attributes <b>Type</b> and <b>Category</b> have been\n");
   rsprintf("replaced by arbitrary attributes. Please replace these two lines with the\n");
   rsprintf("following entries:<p>\n");
   rsprintf("<pre>\n");
   rsprintf("Attributes = Author, Type, Category, Subject\n");
   rsprintf("Required Attributes = Author\n");
   getcfg(lbs->name, "Types", str, sizeof(str));
   rsprintf("Options Type = %s\n", str);
   getcfg(lbs->name, "Categories", str, sizeof(str));
   rsprintf("Options Category = %s\n", str);
   rsprintf("Page title = $subject\n");
   rsprintf("</pre>\n");
   rsprintf("<p>\n");

   rsprintf("It is of course possible to change the attributes or add new ones. The new\n");
   rsprintf("options in the configuration file are described under <a href=\"\n");
   rsprintf("http://midas.psi.ch/elog/config.html\">http://midas.psi.ch/elog/config.html\n");
   rsprintf("</a>.\n");

   rsprintf("</td></tr></table>\n\n");

   rsprintf("<hr>\n");
   rsprintf("<address>\n");
   rsprintf("<a href=\"http://midas.psi.ch/~stefan\">S. Ritt</a>, 18 October 2001");
   rsprintf("</address>");
   show_bottom_text(lbs);
   rsprintf("</body></html>\r\n");
}

/*------------------------------------------------------------------*/

LBLIST *get_subgroup(LBLIST pgrp, char *logbook)
/* retrieve parent of group member "logbook" (which might be group by itself) */
{
   int i;

   for (i = 0; i < pgrp->n_members; i++) {
      /* check if logbook is current member */
      if (strieq(logbook, pgrp->member[i]->name))
         return &(pgrp->member[i]);

      /* check if logbook is in subgroup of current member */
      if (pgrp->member[i]->n_members > 0 && get_subgroup(pgrp->member[i], logbook))
         return get_subgroup(pgrp->member[i], logbook);
   }

   return NULL;
}

/*------------------------------------------------------------------*/

LBLIST get_logbook_hierarchy(void)
{
   int i, j, n, m, flag;
   char str[1000], grpname[256], grpmembers[1000];
   LBLIST root, *pgrp;
   char grplist[MAX_N_LIST][NAME_LENGTH];

   /* allocate root node */
   root = xmalloc(sizeof(LBNODE));
   memset(root, 0, sizeof(LBNODE));

   /* enumerate groups */
   for (i = n = 0;; i++) {
      if (!enumcfg("global", grpname, sizeof(grpname), grpmembers, sizeof(grpmembers), i))
         break;

      /* flag indicates top group (2) or group (1) or other entry (0) */
      flag = 0;
      strlcpy(str, grpname, sizeof(str));
      str[9] = 0;
      if (strieq(str, "top group"))
         flag = 2;
      str[5] = 0;
      if (strieq(str, "group"))
         flag = 1;

      if (flag) {

         /* allocate new node, increase member pointer array by one */
         if (n == 0)
            root->member = xmalloc(sizeof(void *));
         else
            root->member = xrealloc(root->member, (n + 1) * sizeof(void *));
         root->member[n] = xmalloc(sizeof(LBNODE));

         if (strlen(grpname) < 7)
            strlcpy(root->member[n]->name, "Invalid group definition!", 256);
         else if (flag == 1)
            strlcpy(root->member[n]->name, grpname + 6, 256);
         else
            strlcpy(root->member[n]->name, grpname + 10, 256);

         m = strbreak(grpmembers, grplist, MAX_N_LIST, ",");
         root->member[n]->n_members = m;

         root->member[n]->member = xcalloc(sizeof(void *), m);
         root->member[n]->n_members = m;
         for (j = 0; j < m; j++) {
            root->member[n]->member[j] = xcalloc(sizeof(LBNODE), 1);
            strlcpy(root->member[n]->member[j]->name, grplist[j], 256);
         }

         root->member[n]->is_top = (flag == 2);

         n++;
      }
   }

   root->n_members = n;

   /* populate nodes with logbooks or other groups */
   for (i = 0; i < root->n_members; i++)
      if (root->member[i]) {

         for (j = 0; j < root->n_members; j++) {
            if (i != j && root->member[j] != NULL &&
                (pgrp = get_subgroup(root->member[j], root->member[i]->name)) != NULL) {

               /* node is allocated twice, so free one... */
               xfree(*pgrp);

               /* ... and reference the other */
               *pgrp = root->member[i];

               /* mark original pointer invalid */
               root->member[i] = NULL;

               break;
            }
         }
      }

   /* remove empty slots */
   for (i = 0; i < root->n_members; i++)
      if (root->member[i] == NULL) {
         for (j = i + 1; j < root->n_members; j++)
            if (root->member[j])
               break;

         if (j < root->n_members && root->member[j]) {
            root->member[i] = root->member[j];
            root->member[j] = NULL;
         }
      }

   for (i = 0; i < root->n_members; i++)
      if (root->member[i] == NULL)
         break;
   if (i < root->n_members)
      root->n_members = i;

   if (n == 0) {
      for (n = 0; lb_list[n].name[0]; n++);

      /* make simple list with logbooks */
      root->member = xcalloc(n, sizeof(void *));
      root->n_members = n;

      for (i = 0; i < n; i++) {
         root->member[i] = xcalloc(1, sizeof(LBNODE));
         strlcpy(root->member[i]->name, lb_list[i].name, 256);
      }
   }

   return root;
}

/*------------------------------------------------------------------*/

void free_logbook_hierarchy(LBLIST root)
{
   int i;

   for (i = 0; i < root->n_members; i++) {
      if (root->member[i]) {
         free_logbook_hierarchy(root->member[i]);
         root->member[i] = NULL;
      }
   }

   xfree(root->member);
   xfree(root);
}

/*------------------------------------------------------------------*/

BOOL is_logbook_in_group(LBLIST pgrp, char *logbook)
/* test if "logbook" is in group node plb */
{
   int i;

   if (strieq(logbook, pgrp->name))
      return TRUE;

   for (i = 0; i < pgrp->n_members; i++) {
      if (strieq(logbook, pgrp->member[i]->name))
         return TRUE;

      if (pgrp->member[i]->n_members > 0 && is_logbook_in_group(pgrp->member[i], logbook))
         return TRUE;
   }

   return FALSE;
}

/*------------------------------------------------------------------*/

void change_logbook_in_group(LOGBOOK * lbs, char *new_name)
{
   int i, j, n, flag;
   char str[1000], grpname[256], grpmembers[1000];
   char grplist[MAX_N_LIST][NAME_LENGTH];

   /* enumerate groups */
   for (i = 0;; i++) {
      if (!enumcfg("global", grpname, sizeof(grpname), grpmembers, sizeof(grpmembers), i))
         break;

      flag = 0;
      strlcpy(str, grpname, sizeof(str));
      str[9] = 0;
      if (strieq(str, "top group"))
         flag = 2;
      str[5] = 0;
      if (strieq(str, "group"))
         flag = 1;

      if (flag) {

         n = strbreak(grpmembers, grplist, MAX_N_LIST, ",");
         for (j = 0; j < n; j++) {
            if (strieq(lbs->name, grplist[j])) {
               /* rename or remove logbook */
               change_config_line(lbs, grpname, lbs->name, new_name);
               break;
            }
         }
      }
   }
}

/*------------------------------------------------------------------*/

void add_logbook_to_group(LOGBOOK * lbs, char *new_name)
{
   int i, j, n, flag;
   char str[1000], grpname[256], grpmembers[1000];
   char grplist[MAX_N_LIST][NAME_LENGTH];

   /* enumerate groups */
   for (i = 0;; i++) {
      if (!enumcfg("global", grpname, sizeof(grpname), grpmembers, sizeof(grpmembers), i))
         break;

      flag = 0;
      strlcpy(str, grpname, sizeof(str));
      str[9] = 0;
      if (strieq(str, "top group"))
         flag = 2;
      str[5] = 0;
      if (strieq(str, "group"))
         flag = 1;

      if (flag) {

         n = strbreak(grpmembers, grplist, MAX_N_LIST, ",");
         for (j = 0; j < n; j++) {
            if (strieq(lbs->name, grplist[j])) {
               /* rename or remove logbook */
               change_config_line(lbs, grpname, "", new_name);
               break;
            }
         }
      }
   }
}

/*------------------------------------------------------------------*/

void show_standard_title(char *logbook, char *text, int printable)
{
   char str[256], ref[256], sclass[32], comment[256];
   int i, j, level;
   LBLIST phier, pnode, pnext, flb;

   if (printable)
      rsprintf("<table class=\"pframe\" cellpadding=0 cellspacing=0><!-- show_standard_title -->\n\n");
   else
      rsprintf("<table class=\"frame\" cellpadding=0 cellspacing=0><!-- show_standard_title -->\n\n");

   /* scan logbook hierarchy */
   phier = get_logbook_hierarchy();

   /*---- logbook selection row ----*/

   pnode = phier;               /* start at root of tree */
   pnext = NULL;

   if (!printable && (!getcfg(logbook, "logbook tabs", str, sizeof(str))
                      || atoi(str) == 1)) {

      for (level = 0;; level++) {
         rsprintf("<tr><td class=\"tabs\">\n");

         if (level == 0 && getcfg("global", "main tab", str, sizeof(str))
             && !getcfg_topgroup())
            rsprintf("<span class=\"ltab\"><a href=\"../\">%s</a></span>\n", str);

         if (level == 1 && getcfg("global", "main tab", str, sizeof(str))
             && getcfg_topgroup())
            rsprintf("<span class=\"ltab\"><a href=\"../%s/\">%s</a></span>\n", getcfg_topgroup(), str);

         /* iterate through members of this group */
         for (i = 0; i < pnode->n_members; i++) {

            if (getcfg(pnode->member[i]->name, "Hidden", str, sizeof(str))
                && atoi(str) == 1)
               continue;

            strlcpy(str, pnode->member[i]->name, sizeof(str));

            /* build reference to first logbook in group */
            comment[0] = 0;
            if (pnode->member[i]->member == NULL) {
               getcfg(ref, "Comment", comment, sizeof(comment));
               strlcpy(ref, str, sizeof(ref));  // current node is logbook
            } else {
               flb = pnode->member[i]->member[0];       // current node is group
               while (flb->member)      // so traverse hierarchy
                  flb = flb->member[0];
               strlcpy(ref, flb->name, sizeof(ref));
            }
            url_encode(ref, sizeof(ref));

            if (is_logbook_in_group(pnode->member[i], logbook)) {

               /* remember member list of this group for next row */
               pnext = pnode->member[i];

               if (pnode->member[i]->member == NULL)
                  /* selected logbook */
                  strcpy(sclass, "sltab");
               else
                  /* selected group */
                  strcpy(sclass, "sgtab");
            } else {
               if (pnode->member[i]->member == NULL)
                  /* unselected logbook */
                  strcpy(sclass, "ltab");
               else
                  /* unselected group */
                  strcpy(sclass, "gtab");
            }

            if (!pnode->member[i]->is_top) {

               rsprintf("<span class=\"%s\">", sclass);

               if (comment[0]) {
                  rsprintf("<a href=\"../%s/\" title=\"", ref);
                  rsputs3(comment);
                  rsprintf("\">");
               } else
                  rsprintf("<a href=\"../%s/\">", ref, comment);

               strlcpy(str, pnode->member[i]->name, sizeof(str));

               for (j = 0; j < (int) strlen(str); j++)
                  if (str[j] == ' ')
                     rsprintf("&nbsp;");
                  else
                     rsprintf("%c", str[j]);

               rsprintf("</a></span>\n");
            }
         }

         rsprintf("</td></tr>\n\n");

         pnode = pnext;
         pnext = NULL;

         if (pnode == NULL)
            break;
      }
   }

   free_logbook_hierarchy(phier);

   /*---- title row ----*/

   rsprintf("<tr><td><table width=\"100%%\" border=0 cellpadding=0 cellspacing=0>\n");

   /* left cell */
   rsprintf("<tr><td class=\"title1\">");

   /* use comment as title if available, else logbook name */
   if (!getcfg(logbook, "Comment", str, sizeof(str)))
      strcpy(str, logbook);

   rsprintf("&nbsp;&nbsp;");
   rsputs3(str);
   rsputs3(text);
   rsprintf("&nbsp;</td>\n");

   /* middle cell */
   if (*getparam("full_name"))
      rsprintf("<td class=\"title2\">%s \"%s\"</td>\n", loc("Logged in as"), getparam("full_name"));
   else if (getcfg(logbook, "Guest menu commands", str, sizeof(str)))
      rsprintf("<td class=\"title2\" align=center>%s</td>\n", loc("Not logged in"));

   /* right cell */
   rsprintf("<td class=\"title3\">");

   if (getcfg(logbook, "Title image URL", str, sizeof(str)))
      rsprintf("<a href=\"%s\">\n", str);

   if (getcfg(logbook, "Title image", str, sizeof(str)))
      rsprintf(str);
   else
      rsprintf("<img border=0 src=\"elog.png\" alt=\"ELOG logo\" title=\"ELOG logo\">");

   if (getcfg(logbook, "Title image URL", str, sizeof(str)))
      rsprintf("</a>\n");

   rsprintf("</td>\n");

   rsprintf("</tr></table></td></tr>\n\n");
}

/*------------------------------------------------------------------*/

void show_top_text(LOGBOOK * lbs)
{
   char str[NAME_LENGTH];
   int size;

   if (getcfg(lbs->name, "top text", str, sizeof(str))) {
      FILE *f;
      char file_name[256], *buf;

      /* check if file starts with an absolute directory */
      if (str[0] == DIR_SEPARATOR || str[1] == ':')
         strcpy(file_name, str);
      else {
         strlcpy(file_name, resource_dir, sizeof(file_name));
         strlcat(file_name, str, sizeof(file_name));
      }

      f = fopen(file_name, "rb");
      if (f != NULL) {
         fseek(f, 0, SEEK_END);
         size = TELL(fileno(f));
         fseek(f, 0, SEEK_SET);

         buf = xmalloc(size + 1);
         fread(buf, 1, size, f);
         buf[size] = 0;
         fclose(f);

         rsputs(buf);
      } else
         rsputs(str);
   }
}

/*------------------------------------------------------------------*/

void show_bottom_text(LOGBOOK * lbs)
{
   char str[NAME_LENGTH];
   int size;

   if (getcfg(lbs->name, "bottom text", str, sizeof(str))) {
      FILE *f;
      char file_name[256], *buf;

      /* check if file starts with an absolute directory */
      if (str[0] == DIR_SEPARATOR || str[1] == ':')
         strcpy(file_name, str);
      else {
         strlcpy(file_name, resource_dir, sizeof(file_name));
         strlcat(file_name, str, sizeof(file_name));
      }

      f = fopen(file_name, "rb");
      if (f != NULL) {
         fseek(f, 0, SEEK_END);
         size = TELL(fileno(f));
         fseek(f, 0, SEEK_SET);

         buf = xmalloc(size + 1);
         fread(buf, 1, size, f);
         buf[size] = 0;
         fclose(f);

         rsputs(buf);
      } else
         rsputs(str);
   } else
      /* add little logo */
      rsprintf
          ("<center><a class=\"bottomlink\" title=\"%s\" href=\"http://midas.psi.ch/elog/\">ELOG V%s</a></center>",
           loc("Goto ELOG home page"), VERSION);
}

/*------------------------------------------------------------------*/

void show_error(char *error)
{
   /* header */
   show_html_header(NULL, FALSE, "ELOG error", TRUE, FALSE, NULL);

   rsprintf("<body><center>\n");
   rsprintf("<table class=\"dlgframe\" width=\"50%%\" cellpadding=1 cellspacing=0");
   rsprintf("<tr><td class=\"errormsg\">%s</td></tr>\n", error);

   rsprintf("<tr><td class=\"errormsg\">");

   /*
      rsprintf("<script language=\"javascript\" type=\"text/javascript\">\n");
      rsprintf("<button type=button onClick=history.back()>%s</button>\n", loc("Back"));
      rsprintf("</script>\n");

      rsprintf("<noscript>\n");
    */

   rsprintf("%s\n", loc("Please use your browser's back button to go back"));

   /*
      rsprintf("</noscript>\n");
    */

   rsprintf("</td></tr>\n</table>\n");
   rsprintf("</center></body></html>\n");
}

/*------------------------------------------------------------------*/

void set_login_cookies(LOGBOOK * lbs, char *user, char *enc_pwd)
{
   char str[256], lb_name[256], exp[80];
   BOOL global;
   int i;

   rsprintf("HTTP/1.1 302 Found\r\n");
   rsprintf("Server: ELOG HTTP %s\r\n", VERSION);
   if (use_keepalive) {
      rsprintf("Connection: Keep-Alive\r\n");
      rsprintf("Keep-Alive: timeout=60, max=10\r\n");
   }

   if (lbs)
      strcpy(lb_name, lbs->name);
   else
      strcpy(lb_name, "global");

   /* get optional expriation from configuration file */
   if (getcfg(lbs->name, "Login expiration", str, sizeof(str)) || atof(str) > 0)
      strcpy(exp, str);
   else if (isparam("remember")) {
      strcpy(exp, "744");       /* one month by default = 31*24 */
   } else
      exp[0] = 0;

   /* check if cookies should be global */
   global = getcfg("global", "Password file", str, sizeof(str));

   /* two cookies for password and user name */
   set_cookie(lbs, "unm", user, global, exp);
   set_cookie(lbs, "upwd", enc_pwd, global, exp);

   if (global &&user[0] == 0 && enc_pwd[0] == 0) {
      /* if logging out global, also delete possible non-global cookies */
      for (i = 0; lb_list[i].name[0]; i++) {
         set_cookie(&lb_list[i], "unm", user, 0, exp);
         set_cookie(&lb_list[i], "upwd", enc_pwd, 0, exp);
      }
   }

   if (user[0]) {
      /* set "remember me" cookie on login */
      if (isparam("remember"))
         set_cookie(lbs, "urem", "1", global, "8760");  /* one year = 24*365 */
      else
         set_cookie(lbs, "urem", "0", global, "8760");
   }

   set_redir(lbs, getparam("redir"));
}

/*------------------------------------------------------------------*/

void remove_all_login_cookies(LOGBOOK *lbs)
{
   int i;

   rsprintf("HTTP/1.1 302 Found\r\n");
   rsprintf("Server: ELOG HTTP %s\r\n", VERSION);
   if (use_keepalive) {
      rsprintf("Connection: Keep-Alive\r\n");
      rsprintf("Keep-Alive: timeout=60, max=10\r\n");
   }

   /* remove global cookies */
   set_cookie(NULL, "unm", "", TRUE, "");
   set_cookie(NULL, "upwd", "", TRUE, "");

   for (i = 0; lb_list[i].name[0]; i++) {
      set_cookie(&lb_list[i], "unm", "", 0, "");
      set_cookie(&lb_list[i], "upwd", "", 0, "");
   }

   set_redir(lbs, getparam("redir"));
}

/*------------------------------------------------------------------*/

int exist_file(char *file_name)
{
   int fh;

   fh = open(file_name, O_RDONLY | O_BINARY);
   if (fh > 0) {
      close(fh);
      return 1;
   }
   return 0;
}

/*------------------------------------------------------------------*/

void send_file_direct(char *file_name)
{
   int fh, i, length, delta;
   char str[MAX_PATH_LENGTH], charset[80], format[80];
   time_t now;
   struct tm *gmt;

   fh = open(file_name, O_RDONLY | O_BINARY);
   if (fh > 0) {
      lseek(fh, 0, SEEK_END);
      length = TELL(fh);
      lseek(fh, 0, SEEK_SET);

      rsprintf("HTTP/1.1 200 Document follows\r\n");
      rsprintf("Server: ELOG HTTP %s\r\n", VERSION);
      rsprintf("Accept-Ranges: bytes\r\n");

      /* set expiration time to one day */
      time(&now);
      now += (int) (3600 * 24);
      gmt = gmtime(&now);
      strcpy(format, "%A, %d-%b-%y %H:%M:%S GMT");
      strftime(str, sizeof(str), format, gmt);
      rsprintf("Expires: %s\r\n", str);

      if (use_keepalive) {
         rsprintf("Connection: Keep-Alive\r\n");
         rsprintf("Keep-Alive: timeout=60, max=10\r\n");
      }

      /* return proper header for file type */
      for (i = 0; i < (int) strlen(file_name); i++)
         str[i] = toupper(file_name[i]);
      str[i] = 0;

      for (i = 0; filetype[i].ext[0]; i++)
         if (chkext(str, filetype[i].ext))
            break;

      if (!getcfg("global", "charset", charset, sizeof(charset)))
         strcpy(charset, DEFAULT_HTTP_CHARSET);

      if (filetype[i].ext[0]) {
         if (strncmp(filetype[i].type, "text", 4) == 0)
            rsprintf("Content-Type: %s;charset=%s\r\n", filetype[i].type, charset);
         else
            rsprintf("Content-Type: %s\r\n", filetype[i].type);
      } else if (is_ascii(file_name))
         rsprintf("Content-Type: text/plain;charset=%s\r\n", charset);
      else
         rsprintf("Content-Type: application/octet-stream;charset=%s\r\n", charset);

      rsprintf("Content-Length: %d\r\n\r\n", length);

      /* increase return buffer size if file too big */
      if (length > return_buffer_size - (int) strlen(return_buffer)) {
         delta = length - (return_buffer_size - strlen(return_buffer)) + 1000;

         return_buffer = xrealloc(return_buffer, return_buffer_size + delta);
         memset(return_buffer + return_buffer_size, 0, delta);
         return_buffer_size += delta;
      }

      return_length = strlen(return_buffer) + length;
      read(fh, return_buffer + strlen(return_buffer), length);

      close(fh);
   } else {
      show_html_header(NULL, FALSE, "404 Not Found", TRUE, FALSE, NULL);

      rsprintf("<body><h1>Not Found</h1>\r\n");
      rsprintf("The requested file <b>%s</b> was not found on this server<p>\r\n", file_name);
      rsprintf("<hr><address>ELOG version %s</address></body></html>\r\n\r\n", VERSION);
      return_length = strlen_retbuf;
      keep_alive = 0;
   }
}

/*------------------------------------------------------------------*/

void strencode(char *text)
{
   int i;

   for (i = 0; i < (int) strlen(text); i++) {
      switch (text[i]) {
      case '\n':
         rsprintf("<br>\n");
         break;
      case '<':
         rsprintf("&lt;");
         break;
      case '>':
         rsprintf("&gt;");
         break;
      case '&':
         rsprintf("&amp;");
         break;
      case '\"':
         rsprintf("&quot;");
         break;
      case ' ':
         rsprintf("&nbsp;");
         break;

         /* the translation for the search highliting */

      case '\001':
         rsprintf("<");
         break;
      case '\002':
         rsprintf(">");
         break;
      case '\003':
         rsprintf("\"");
         break;
      case '\004':
         rsprintf(" ");
         break;

      default:
         rsprintf("%c", text[i]);
      }
   }
}

/*------------------------------------------------------------------*/

void xmlencode(char *text)
{
   int i;

   for (i = 0; i < (int) strlen(text); i++) {
      switch (text[i]) {
      case '<':
         rsprintf("&lt;");
         break;
      case '>':
         rsprintf("&gt;");
         break;
      case '&':
         rsprintf("&amp;");
         break;
      case '\"':
         rsprintf("&quot;");
         break;

      default:
         rsprintf("%c", text[i]);
      }
   }
}

/*------------------------------------------------------------------*/

void strencode2(char *b, char *text)
{
   int i;

   *b = 0;
   for (i = 0; i < (int) strlen(text); i++) {
      switch (text[i]) {
      case '\n':
         strcat(b, "<br>\n");
         break;
      case '<':
         strcat(b, "&lt;");
         break;
      case '>':
         strcat(b, "&gt;");
         break;
      case '&':
         strcat(b, "&amp;");
         break;
      case '\"':
         strcat(b, "&quot;");
         break;
      default:
         sprintf(b + strlen(b), "%c", text[i]);
      }
   }
}

/*------------------------------------------------------------------*/

int build_subst_list(LOGBOOK * lbs, char list[][NAME_LENGTH], char value[][NAME_LENGTH],
                     char attrib[][NAME_LENGTH], BOOL format_date)
{
   int i;
   char str[NAME_LENGTH], format[256];
   time_t t;
   struct tm *ts;

   /* copy attribute list */
   i = 0;
   if (attrib != NULL)
      for (; i < lbs->n_attr; i++) {
         strcpy(list[i], attr_list[i]);
         if (attrib) {
            if ((attr_flags[i] & (AF_DATE | AF_DATETIME)) && format_date) {

               t = (time_t) atoi(attrib[i]);
               ts = localtime(&t);
               if (!getcfg(lbs->name, "Date format", format, sizeof(format)))
                  strcpy(format, DEFAULT_DATE_FORMAT);

               my_strftime(value[i], NAME_LENGTH, format, ts);

            } else
               strcpy(value[i], attrib[i]);
         } else
            strcpy(value[i], getparam(attr_list[i]));
      }

   /* add remote host */
   strcpy(list[i], "remote_host");
   strlcpy(value[i++], rem_host, NAME_LENGTH);

   /* add local host */
   strcpy(list[i], "host");
   strlcpy(value[i++], host_name, NAME_LENGTH);

   /* add user names */
   strcpy(list[i], "short_name");
   strlcpy(value[i++], getparam("unm"), NAME_LENGTH);
   strcpy(list[i], "long_name");
   strlcpy(value[i++], getparam("full_name"), NAME_LENGTH);

   /* add email */
   strcpy(list[i], "user_email");
   strcpy(value[i], "mailto:");
   strlcat(value[i++], getparam("user_email"), NAME_LENGTH);

   /* add logbook */
   strcpy(list[i], "logbook");
   strlcpy(value[i++], lbs->name, NAME_LENGTH);

   /* add logbook */
   strcpy(list[i], "elogbook");
   strlcpy(value[i++], lbs->name_enc, NAME_LENGTH);

   /* add date */
   strcpy(list[i], "date");
   time(&t);
   if (format_date) {
      ts = localtime(&t);
      if (!getcfg(lbs->name, "Time format", format, sizeof(format)))
         strcpy(format, DEFAULT_TIME_FORMAT);

      my_strftime(str, sizeof(str), format, ts);
   } else
      sprintf(str, "%d", (int) t);
   strcpy(value[i++], str);

   /* add UTC date */
   strcpy(list[i], "utcdate");
   time(&t);
   if (format_date) {
      ts = gmtime(&t);
      if (!getcfg(lbs->name, "Time format", format, sizeof(format)))
         strcpy(format, DEFAULT_TIME_FORMAT);

      my_strftime(str, sizeof(str), format, ts);
   } else
      sprintf(str, "%d", (int) t);
   strcpy(value[i++], str);

   return i;
}

/*------------------------------------------------------------------*/

void add_subst_list(char list[][NAME_LENGTH], char value[][NAME_LENGTH], char *item, char *str, int *i)
{
   strcpy(list[*i], item);
   strcpy(value[(*i)++], str);
}

void add_subst_time(LOGBOOK * lbs,
                    char list[][NAME_LENGTH], char value[][NAME_LENGTH], char *item, char *date, int *i)
{
   char format[80], str[256];
   time_t ltime;
   struct tm *pts;

   if (!getcfg(lbs->name, "Time format", format, sizeof(format)))
      strcpy(format, DEFAULT_TIME_FORMAT);
   ltime = date_to_ltime(date);
   pts = localtime(&ltime);
   my_strftime(str, sizeof(str), format, pts);

   add_subst_list(list, value, item, str, i);
}

/*------------------------------------------------------------------*/

BOOL get_password_file(LOGBOOK * lbs, char *file_name, int size)
{
   char str[256];

   getcfg(lbs->name, "Password file", str, sizeof(str));

   if (!str[0])
      return FALSE;

   if (str[0] == DIR_SEPARATOR || str[1] == ':')
      strlcpy(file_name, str, size);
   else {
      strlcpy(file_name, resource_dir, size);
      strlcat(file_name, str, size);
   }

   return TRUE;
}

/*------------------------------------------------------------------*/

BOOL change_pwd(LOGBOOK * lbs, char *user, char *pwd)
{
   char str[256], file_name[256];
   PMXML_NODE node;

   if (!lbs->pwd_xml_tree)
      return FALSE;

   sprintf(str, "/list/user[name=%s]/password", user);
   node = mxml_find_node(lbs->pwd_xml_tree, str);
   if (node == NULL)
      return FALSE;

   mxml_replace_node_value(node, pwd);

   if (get_password_file(lbs, file_name, sizeof(file_name)))
      mxml_write_tree(file_name, lbs->pwd_xml_tree);

   return TRUE;
}

/*------------------------------------------------------------------*/

void show_change_pwd_page(LOGBOOK * lbs)
{
   char str[256], old_pwd[32], new_pwd[32], new_pwd2[32], act_pwd[32], user[80];
   int wrong_pwd;

   old_pwd[0] = new_pwd[0] = new_pwd2[0] = 0;

   if (isparam("oldpwd"))
      do_crypt(getparam("oldpwd"), old_pwd);
   if (isparam("newpwd"))
      do_crypt(getparam("newpwd"), new_pwd);
   if (isparam("newpwd2"))
      do_crypt(getparam("newpwd2"), new_pwd2);

   strcpy(user, getparam("unm"));
   if (isparam("config"))
      strcpy(user, getparam("config"));

   wrong_pwd = FALSE;

   if (old_pwd[0] || new_pwd[0]) {
      if (user[0]
          && get_user_line(lbs, user, act_pwd, NULL, NULL, NULL, NULL)) {

         /* administrator does not have to supply old password if changing other user's password */
         if (is_admin_user(lbs->name, getparam("unm"))
             && stricmp(getparam("unm"), user) != 0)
            wrong_pwd = 0;
         else {
            if (strcmp(old_pwd, act_pwd) != 0)
               wrong_pwd = 1;
         }

         if (strcmp(new_pwd, new_pwd2) != 0)
            wrong_pwd = 2;
      }

      if (new_pwd[0]) {
         /* replace password */
         if (!wrong_pwd)
            change_pwd(lbs, user, new_pwd);

         if (!wrong_pwd && strcmp(user, getparam("unm")) == 0) {
            set_login_cookies(lbs, user, new_pwd);
            return;
         }

         if (!wrong_pwd) {
            /* redirect back to configuration page */
            sprintf(str, "?cmd=%s&cfg_user=%s", loc("Config"), getparam("config"));
            redirect(lbs, str);
            return;
         }
      }
   }

   show_standard_header(lbs, TRUE, loc("ELOG change password"), NULL, FALSE, NULL);

   rsprintf("<table class=\"dlgframe\" cellspacing=0 align=center>");

   if (wrong_pwd == 1)
      rsprintf("<tr><td colspan=2 class=\"dlgerror\">%s!</td></tr>\n", loc("Wrong password"));

   if (wrong_pwd == 2)
      rsprintf("<tr><td colspan=2 class=\"dlgerror\">%s!</td></tr>\n",
               loc("New passwords do not match, please retype"));

   rsprintf("<tr><td colspan=2 class=\"dlgtitle\">\n");

   rsprintf("<input type=hidden name=config value=\"%s\">", user);

   rsprintf("%s \"%s\"</td></tr>\n", loc("Change password for user"), user);

   /* do not ask for old pwasword if admin changes other user's password */
   if (!is_admin_user(lbs->name, getparam("unm")) || stricmp(getparam("unm"), user) == 0) {
      if (isparam("oldpwd") && !(wrong_pwd == 1))
         rsprintf("<input type=hidden name=oldpwd value=\"%s\"", getparam("oldpwd"));
      else {
         rsprintf("<tr><td align=right class=\"dlgform\">%s:\n", loc("Old password"));
         rsprintf("<td align=left class=\"dlgform\"><input type=password name=oldpwd>\n");
         rsprintf("</td></tr>\n");
      }
   }

   rsprintf("<tr><td align=right class=\"dlgform\">%s:</td>\n", loc("New password"));
   rsprintf("<td align=left class=\"dlgform\"><input type=password name=newpwd></td></tr>\n");

   rsprintf("<tr><td align=right class=\"dlgform\">%s:</td>\n", loc("Retype new password"));
   rsprintf("<td align=left class=\"dlgform\"><input type=password name=newpwd2></td></tr>\n");

   rsprintf
       ("<tr><td align=center colspan=2 class=\"dlgform\"><input type=submit value=\"%s\"></td></tr>",
        loc("Submit"));

   rsprintf("</table>\n");
   show_bottom_text(lbs);
   rsprintf("</form></body></html>\r\n");
}

/*------------------------------------------------------------------*/

int get_last_index(LOGBOOK * lbs, int index)
/* return value of specific attribute of last entry, can be used to
auto-increment tags */
{
   int i, message_id;
   char str[80], attrib[MAX_N_ATTR][NAME_LENGTH], att[MAX_ATTACHMENTS][256];

   str[0] = 0;
   message_id = el_search_message(lbs, EL_LAST, 0, FALSE);

   if (!message_id)
      return 0;

   el_retrieve(lbs, message_id, NULL, attr_list, attrib, lbs->n_attr, NULL, 0, NULL, NULL, att, NULL, NULL);

   strcpy(str, attrib[index]);

   /* look for first digit, return value */
   for (i = 0; i < (int) strlen(str); i++)
      if (isdigit(str[i]))
         break;

   return atoi(str + i);
}

/*------------------------------------------------------------------*/

BOOL is_author(LOGBOOK * lbs, char attrib[MAX_N_ATTR][NAME_LENGTH], char *owner)
{
   char str[NAME_LENGTH], preset[NAME_LENGTH];
   int i;

   /* check if current user is admin */
   if (is_admin_user(lbs->name, getparam("unm")))
      return TRUE;

   /* search attribute which contains short_name of author */
   for (i = 0; i < lbs->n_attr; i++) {
      sprintf(str, "Preset %s", attr_list[i]);
      if (getcfg(lbs->name, str, preset, sizeof(preset))) {
         if (strstr(preset, "$short_name")) {
            if (strstr(attrib[i], getparam("unm")) == NULL) {
               strcpy(owner, attrib[i]);
               return FALSE;
            } else
               break;
         }
      }
   }

   if (i == lbs->n_attr) {
      /* if not found, search attribute which contains full_name of author */
      for (i = 0; i < lbs->n_attr; i++) {
         sprintf(str, "Preset %s", attr_list[i]);
         if (getcfg(lbs->name, str, preset, sizeof(preset))) {
            if (strstr(preset, "$long_name")) {
               if (strstr(attrib[i], getparam("full_name")) == NULL) {
                  strcpy(owner, attrib[i]);
                  return FALSE;
               }
            }
         }
      }
   }

   return TRUE;
}

/*------------------------------------------------------------------*/

BOOL get_author(LOGBOOK * lbs, char attrib[MAX_N_ATTR][NAME_LENGTH], char *author)
{
   char str[NAME_LENGTH], preset[NAME_LENGTH];
   int i;

   /* search attribute which contains full_name of author */
   for (i = 0; i < lbs->n_attr; i++) {
      sprintf(str, "Preset %s", attr_list[i]);
      if (getcfg(lbs->name, str, preset, sizeof(preset))) {
         if (stristr(preset, "$long_name")) {
            strcpy(author, attrib[i]);
            return TRUE;
         }
      }
   }

   /* if not found, search attribute which contains short_name of author */
   for (i = 0; i < lbs->n_attr; i++) {
      sprintf(str, "Preset %s", attr_list[i]);
      if (getcfg(lbs->name, str, preset, sizeof(preset))) {
         if (stristr(preset, "$short_name")) {
            strcpy(author, attrib[i]);
            return TRUE;
         }
      }
   }

   return FALSE;
}

/*------------------------------------------------------------------*/

BOOL is_cond_attr(int index)
{
   int i;

   for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++)
      if (strchr(attr_options[index][i], '{') && strchr(attr_options[index][i], '}'))
         return TRUE;

   return FALSE;
}

/*------------------------------------------------------------------*/

void show_date_selector(int day, int month, int year, char *index)
{
   int i;

   rsprintf("<select name=\"m%s\">\n", index);

   rsprintf("<option value=\"\">\n");
   for (i = 0; i < 12; i++)
      if (i + 1 == month)
         rsprintf("<option selected value=\"%d\">%s\n", i + 1, month_name(i));
      else
         rsprintf("<option value=\"%d\">%s\n", i + 1, month_name(i));
   rsprintf("</select>\n");

   rsprintf("<select name=\"d%s\">", index);
   rsprintf("<option selected value=\"\">\n");
   for (i = 0; i < 31; i++)
      if (i + 1 == day)
         rsprintf("<option selected value=%d>%d\n", i + 1, i + 1);
      else
         rsprintf("<option value=%d>%d\n", i + 1, i + 1);
   rsprintf("</select>\n");

   if (year)
      rsprintf
          ("&nbsp;%s: <input type=\"text\" size=5 maxlength=5 name=\"y%s\" value=\"%d\">",
           loc("Year"), index, year);
   else
      rsprintf("&nbsp;%s: <input type=\"text\" size=5 maxlength=5 name=\"y%s\">", loc("Year"), index);

   rsprintf("\n<script language=\"javascript\" type=\"text/javascript\">\n");
   rsprintf("<!--\n");
   rsprintf("function opencal(i)\n");
   rsprintf("{\n");
   rsprintf("  window.open(\"cal.html?i=\"+i, \"\",\n");
   rsprintf("  \"top=280,left=350,width=300,height=195,dependent=yes,menubar=no,status=no,");
   rsprintf("scrollbars=no,location=no,resizable=yes\");\n");
   rsprintf("}\n\n");

   rsprintf("  document.write(\"&nbsp;&nbsp;\");\n");
   rsprintf("  document.write(\"<a href=\\\"javascript:opencal('%s')\\\">\");\n", index);
   rsprintf("  document.writeln(\"<img src=\\\"cal.png\\\" align=\\\"middle\\\" border=\\\"0\\\"");
   rsprintf("alt=\\\"%s\\\"></a>\");\n", loc("Pick a date"));

   rsprintf("//-->\n");
   rsprintf("</script>&nbsp;&nbsp;\n");
}

/*------------------------------------------------------------------*/

void show_time_selector(int hour, int min, int sec, char *index)
{
   int i;

   rsprintf("<select name=\"h%s\">\n", index);
   rsprintf("<option value=\"\">\n");
   for (i = 0; i < 24; i++)
      if (i == hour)
         rsprintf("<option selected value=\"%d\">%02d\n", i, i);
      else
         rsprintf("<option value=\"%d\">%02d\n", i, i);
   rsprintf("</select>&nbsp;<b>:</b>&nbsp;\n");

   rsprintf("<select name=\"n%s\">", index);
   rsprintf("<option selected value=\"\">\n");
   for (i = 0; i < 60; i++)
      if (i == min)
         rsprintf("<option selected value=%d>%02d\n", i, i);
      else
         rsprintf("<option value=%d>%02d\n", i, i);
   rsprintf("</select>&nbsp;<b>:</b>&nbsp;\n");

   rsprintf("<select name=\"s%s\">", index);
   rsprintf("<option selected value=\"\">\n");
   for (i = 0; i < 60; i++)
      if (i == sec)
         rsprintf("<option selected value=%d>%02d\n", i, i);
      else
         rsprintf("<option value=%d>%02d\n", i, i);
   rsprintf("</select>\n");

   rsprintf("\n<script language=\"javascript\" type=\"text/javascript\">\n");
   rsprintf("<!--\n");
   rsprintf("function settime_%s()\n", index);
   rsprintf("{\n");
   rsprintf("  var d = new Date();\n");
   rsprintf("  document.form1.d%s.value = d.getDate();\n", index);
   rsprintf("  document.form1.m%s.value = d.getMonth()+1;\n", index);
   rsprintf("  year = d.getYear();\n");
   rsprintf("  if (year < 1900)\n");
   rsprintf("     year += 1900;\n");
   rsprintf("  document.form1.y%s.value = year;\n", index);
   rsprintf("  document.form1.h%s.value = d.getHours();\n", index);
   rsprintf("  document.form1.n%s.value = d.getMinutes();\n", index);
   rsprintf("  document.form1.s%s.value = d.getSeconds();\n", index);
   rsprintf("}\n\n");

   rsprintf("  document.write(\"&nbsp;&nbsp;\");\n");
   rsprintf("  document.write(\"<a href=\\\"javascript:settime_%s()\\\">\");\n", index);
   rsprintf("  document.writeln(\"<img src=\\\"clock.png\\\" align=\\\"middle\\\" border=\\\"0\\\" ");
   rsprintf("alt=\\\"%s\\\"></a>\");\n", loc("Insert current time"));

   rsprintf("//-->\n");
   rsprintf("</script>\n");

}

/*------------------------------------------------------------------*/

void attrib_from_param(int n_attr, char attrib[MAX_N_ATTR][NAME_LENGTH])
{
   int i, j, first, year, month, day, hour, min, sec;
   char str[NAME_LENGTH], ua[NAME_LENGTH];
   time_t ltime;
   struct tm ts;

   for (i = 0; i < n_attr; i++) {
      strcpy(ua, attr_list[i]);
      btou(ua);
      if (attr_flags[i] & AF_MULTI) {
         attrib[i][0] = 0;
         first = 1;
         for (j = 0; j < MAX_N_LIST; j++) {
            sprintf(str, "%s_%d", ua, j);
            if (getparam(str)) {
               if (*getparam(str)) {
                  if (first)
                     first = 0;
                  else
                     strlcat(attrib[i], " | ", NAME_LENGTH);
                  if (strlen(attrib[i]) + strlen(getparam(str)) < NAME_LENGTH - 2)
                     strlcat(attrib[i], getparam(str), NAME_LENGTH);
                  else
                     break;
               }
            } else
               break;
         }
      } else if (attr_flags[i] & AF_DATE) {

         sprintf(str, "y%d", i);
         year = atoi(getparam(str));
         if (year < 100)
            year += 2000;

         sprintf(str, "m%d", i);
         month = atoi(getparam(str));

         sprintf(str, "d%d", i);
         day = atoi(getparam(str));

         memset(&ts, 0, sizeof(struct tm));
         ts.tm_year = year - 1900;
         ts.tm_mon = month - 1;
         ts.tm_mday = day;
         ts.tm_hour = 12;

         if (month && day) {
            ltime = mktime(&ts);
            sprintf(attrib[i], "%d", (int) ltime);
         } else
            strcpy(attrib[i], "");

      } else if (attr_flags[i] & AF_DATETIME) {

         sprintf(str, "y%d", i);
         year = atoi(getparam(str));
         if (year < 100)
            year += 2000;

         sprintf(str, "m%d", i);
         month = atoi(getparam(str));

         sprintf(str, "d%d", i);
         day = atoi(getparam(str));

         sprintf(str, "h%d", i);
         hour = atoi(getparam(str));

         sprintf(str, "n%d", i);
         min = atoi(getparam(str));

         sprintf(str, "s%d", i);
         sec = atoi(getparam(str));

         memset(&ts, 0, sizeof(struct tm));
         ts.tm_year = year - 1900;
         ts.tm_mon = month - 1;
         ts.tm_mday = day;
         ts.tm_hour = hour;
         ts.tm_min = min;
         ts.tm_sec = sec;
         ts.tm_isdst = -1;

         if (month && day) {
            ltime = mktime(&ts);
            sprintf(attrib[i], "%d", (int) ltime);
         } else
            strcpy(attrib[i], "");

      } else {
         strlcpy(attrib[i], getparam(ua), NAME_LENGTH);
      }
   }
}

/*------------------------------------------------------------------*/

void ricon(char *name, char *comment, char *onclick)
{
   rsprintf("<img align=\"middle\" name=\"%s\" src=\"icons/elc_%s.png\" alt=\"%s\" title=\"%s\" border=\"0\"",
            name, name, comment, comment);
   rsprintf(" onclick=\"%s\"", onclick);
   rsprintf(" onmousedown=\"document.images.%s.src='icons/eld_%s.png'\"", name, name);
   rsprintf(" onmouseup=\"document.images.%s.src='icons/elc_%s.png'\"", name, name);
   rsprintf(" onmouseover=\"this.style.cursor='hand';\" />");
}

/*------------------------------------------------------------------*/

void rsicon(char *name, char *comment, char *elcode)
{
   rsprintf("<img align=\"middle\" name=\"%s\" src=\"icons/elc_%s.png\" alt=\"%s\" title=\"%s\" border=\"0\"",
            name, name, comment, comment);
   rsprintf(" onclick=\"elcode(document.form1.Text, '','%s')\"", elcode);
   rsprintf(" onmousedown=\"document.images.%s.src='icons/eld_%s.png'\"", name, name);
   rsprintf(" onmouseup=\"document.images.%s.src='icons/elc_%s.png'\"", name, name);
   rsprintf(" onmouseover=\"this.style.cursor='hand';\" />");
}

/*------------------------------------------------------------------*/

void show_edit_form(LOGBOOK * lbs, int message_id, BOOL breply, BOOL bedit, BOOL bupload, BOOL breedit,
                    BOOL bduplicate, BOOL preview)
{
   int i, j, n, index, aindex, size, width, height, fh, length, input_size, input_maxlen,
       format_flags[MAX_N_ATTR], year, month, day, hour, min, sec, n_attr, n_disp_attr,
       attr_index[MAX_N_ATTR], enc_selected, show_smileys;
   char str[2 * NAME_LENGTH], preset[2 * NAME_LENGTH], *p, *pend, star[80], comment[10000], reply_string[256],
       list[MAX_N_ATTR][NAME_LENGTH], file_name[256], *buffer, format[256], date[80], script[256],
       attrib[MAX_N_ATTR][NAME_LENGTH], *text, orig_tag[80], reply_tag[MAX_REPLY_TO * 10],
       att[MAX_ATTACHMENTS][256], encoding[80], slist[MAX_N_ATTR + 10][NAME_LENGTH],
       svalue[MAX_N_ATTR + 10][NAME_LENGTH], owner[256], locked_by[256], class_value[80], class_name[80],
       ua[NAME_LENGTH], mid[80], title[256], login_name[256], cookie[256], orig_author[256];
   time_t now, ltime;
   char fl[8][NAME_LENGTH];
   struct tm *pts;
   BOOL preset_text, subtable;

   for (i = 0; i < MAX_ATTACHMENTS; i++)
      att[i][0] = 0;

   for (i = 0; i < lbs->n_attr; i++)
      attrib[i][0] = 0;

   text = xmalloc(TEXT_SIZE);
   text[0] = 0;
   orig_author[0] = 0;
   encoding[0] = 0;
   date[0] = 0;

   if (breedit || bupload) {
      /* get date from parameter */
      if (*getparam("entry_date"))
         strcpy(date, getparam("entry_date"));

      /* get attributes from parameters */
      attrib_from_param(lbs->n_attr, attrib);

      strlcpy(text, getparam("text"), TEXT_SIZE);

      for (i = 0; i < MAX_ATTACHMENTS; i++) {
         sprintf(str, "attachment%d", i);
         if (isparam(str))
            strlcpy(att[i], getparam(str), 256);
      }

      if (isparam("inlineatt")) {
         for (i = 0; i < MAX_ATTACHMENTS; i++) {
            sprintf(str, "attachment%d", i);
            if (!isparam(str)) {
               strlcpy(att[i], getparam("inlineatt"), 256);
               break;
            }
         }
      }

      /* get encoding */
      strcpy(encoding, getparam("encoding"));
   } else {
      if (message_id) {
         /* get message for reply/edit */

         size = TEXT_SIZE;
         el_retrieve(lbs, message_id, date, attr_list, attrib, lbs->n_attr,
                     text, &size, orig_tag, reply_tag, att, encoding, locked_by);

         get_author(lbs, attrib, orig_author);
      }
   }

   /* Determine encoding */
   enc_selected = 0;            /* Default is ELCode */
   /* Overwrite from config file */
   if (getcfg(lbs->name, "Default Encoding", str, sizeof(str)))
      enc_selected = atoi(str);

   /* detrmine if smiley bar should be displayed */
   show_smileys = 0;
   cookie[0] = 0;
   if (isparam("hsm") && atoi(getparam("hsm")) == 1)    /* cookie */
      show_smileys = 0;
   if (isparam("smcmd") && strieq(getparam("smcmd"), "hsm")) {  /* turn off */
      show_smileys = 0;
      strcpy(cookie, "hsm=1");
   }
   if (isparam("smcmd") && strieq(getparam("smcmd"), "ssm")) {  /* turn on */
      show_smileys = 1;
      strcpy(cookie, "hsm=0");
   }

   /* Overwrite from current entry */
   if (encoding[0]) {
      if (encoding[0] == 'E')
         enc_selected = 0;
      else if (encoding[0] == 'p')
         enc_selected = 1;
      else if (encoding[0] == 'H')
         enc_selected = 2;
   }

   /* check for preset attributes without any condition */
   set_condition("");
   for (index = 0; index < lbs->n_attr; index++) {

      /* check for preset string */
      sprintf(str, "Preset %s", attr_list[index]);
      if ((i = getcfg(lbs->name, str, preset, sizeof(preset))) > 0) {

         if ((!bedit && !breply) ||     /* don't subst on edit or reply */
             (breedit && i == 2)) {     /* subst on reedit only if preset is under condition */

            /* do not format date for date attributes */
            i = build_subst_list(lbs, slist, svalue, attrib,
                                 (attr_flags[index] & (AF_DATE | AF_DATETIME)) == 0);
            strsubst(preset, sizeof(preset), slist, svalue, i);

            /* check for index substitution */
            if (!bedit && strchr(preset, '%')) {
               /* get index */
               i = get_last_index(lbs, index);

               strcpy(str, preset);
               sprintf(preset, str, i + 1);
            }

            if (!strchr(preset, '%'))
               strcpy(attrib[index], preset);
         }
      }

      sprintf(str, "Preset on reply %s", attr_list[index]);
      if ((i = getcfg(lbs->name, str, preset, sizeof(preset))) > 0 && breply) {

         if (!breedit || (breedit && i == 2)) { /* subst on reedit only if preset is under condition */

            /* do not format date for date attributes */
            i = build_subst_list(lbs, slist, svalue, attrib,
                                 (attr_flags[index] & (AF_DATE | AF_DATETIME)) == 0);
            strsubst(preset, sizeof(preset), slist, svalue, i);

            /* check for index substitution */
            if (!bedit && strchr(preset, '%')) {
               /* get index */
               i = get_last_index(lbs, index);

               strcpy(str, preset);
               sprintf(preset, str, i + 1);
            }

            if (!strchr(preset, '%'))
               strcpy(attrib[index], preset);
         }
      }

      /* check for p<attribute> */
      sprintf(str, "p%s", attr_list[index]);
      if (isparam(str))
         strlcpy(attrib[index], getparam(str), NAME_LENGTH);
   }

   /* evaluate conditional attributes */
   evaluate_conditions(lbs, attrib);

   /* rescan attributes if condition set */
   if (_condition[0]) {
      n_attr = scan_attributes(lbs->name);
      if (breedit)
         attrib_from_param(n_attr, attrib);
   } else
      n_attr = lbs->n_attr;

   /* now check again for conditional preset */
   for (index = 0; index < lbs->n_attr; index++) {

      /* check for preset string */
      sprintf(str, "Preset %s", attr_list[index]);
      if ((i = getcfg(lbs->name, str, preset, sizeof(preset))) > 0) {

         if ((!bedit && !breply) ||     /* don't subst on edit or reply */
             (breedit && i == 2)) {     /* subst on reedit only if preset is under condition */

            /* do not format date for date attributes */
            i = build_subst_list(lbs, slist, svalue, attrib,
                                 (attr_flags[index] & (AF_DATE | AF_DATETIME)) == 0);
            strsubst(preset, sizeof(preset), slist, svalue, i);

            /* check for index substitution */
            if (!bedit && strchr(preset, '%')) {
               /* get index */
               i = get_last_index(lbs, index);

               strcpy(str, preset);
               sprintf(preset, str, i + 1);
            }

            if (!strchr(preset, '%'))
               strcpy(attrib[index], preset);
         }
      }

      sprintf(str, "Preset on reply %s", attr_list[index]);
      if ((i = getcfg(lbs->name, str, preset, sizeof(preset))) > 0 && breply) {

         if (!breedit || (breedit && i == 2)) { /* subst on reedit only if preset is under condition */

            /* do not format date for date attributes */
            i = build_subst_list(lbs, slist, svalue, attrib,
                                 (attr_flags[index] & (AF_DATE | AF_DATETIME)) == 0);
            strsubst(preset, sizeof(preset), slist, svalue, i);

            /* check for index substitution */
            if (!bedit && strchr(preset, '%')) {
               /* get index */
               i = get_last_index(lbs, index);

               strcpy(str, preset);
               sprintf(preset, str, i + 1);
            }

            if (!strchr(preset, '%'))
               strcpy(attrib[index], preset);
         }
      }
   }

   /* check for maximum number of replies */
   if (breply) {
      i = 0;
      p = strtok(reply_tag, ",");
      while (p) {
         i++;
         p = strtok(NULL, ",");
      }

      if (i >= MAX_REPLY_TO) {
         sprintf(str, loc("Maximum number of replies (%d) exceeded"), MAX_REPLY_TO);
         show_error(str);
         xfree(text);
         return;
      }
   }

   /* check for author */
   if (bedit && getcfg(lbs->name, "Restrict edit", str, sizeof(str))
       && atoi(str) == 1) {
      if (!is_author(lbs, attrib, owner)) {
         sprintf(str, loc("Only user <i>%s</i> can edit this entry"), owner);
         show_error(str);
         xfree(text);
         return;
      }
   }

   /* check for editing interval */
   if (bedit && getcfg(lbs->name, "Restrict edit time", str, sizeof(str))) {
      for (i = 0; i < *lbs->n_el_index; i++)
         if (lbs->el_index[i].message_id == message_id)
            break;

      if (i < *lbs->n_el_index && time(NULL) > lbs->el_index[i].file_time + atof(str) * 3600) {
         sprintf(str, loc("Entry can only be edited %1.2lg hours after creation"), atof(str));
         show_error(str);
         xfree(text);
         return;
      }
   }

   /* check for locking */
   if (message_id && bedit && !breedit && !bupload) {
      if (getcfg(lbs->name, "Use Lock", str, sizeof(str)) && atoi(str) == 1) {
         if (*getparam("full_name"))
            strcpy(str, getparam("full_name"));
         else
            strcpy(str, loc("user"));

         strcat(str, " ");
         strcat(str, loc("on"));
         strcat(str, " ");
         strcat(str, rem_host);

         el_lock_message(lbs, message_id, str);
      }
   }

   /* remove attributes for replies */
   if (breply) {
      getcfg(lbs->name, "Remove on reply", str, sizeof(str));
      n = strbreak(str, list, MAX_N_ATTR, ",");
      for (i = 0; i < n; i++)
         for (j = 0; j < n_attr; j++) {
            if (strieq(attr_list[j], list[i]))
               attrib[j][0] = 0;
         }
   }

   /* subst attributes for replies */
   if (breply) {
      for (index = 0; index < n_attr; index++) {
         sprintf(str, "Subst on reply %s", attr_list[index]);
         if (getcfg(lbs->name, str, preset, sizeof(preset))) {
            /* check if already second reply */
            if (orig_tag[0] == 0) {

               /* do not format date for date attributes */
               i = build_subst_list(lbs, slist, svalue, attrib,
                                    (attr_flags[index] & (AF_DATE | AF_DATETIME)) == 0);
               sprintf(str, "%d", message_id);
               add_subst_list(slist, svalue, "message id", str, &i);
               add_subst_time(lbs, slist, svalue, "entry time", date, &i);
               strsubst(preset, sizeof(preset), slist, svalue, i);
               strcpy(attrib[index], preset);
            }
         }
      }
   }

   /* subst attributes for edits */
   if (message_id && bedit && !breedit && !bupload) {
      for (index = 0; index < n_attr; index++) {
         sprintf(str, "Subst on edit %s", attr_list[index]);
         if (getcfg(lbs->name, str, preset, sizeof(preset))) {

            /* do not format date for date attributes */
            i = build_subst_list(lbs, slist, svalue, attrib,
                                 (attr_flags[index] & (AF_DATE | AF_DATETIME)) == 0);

            sprintf(str, "%d", message_id);
            add_subst_list(slist, svalue, "message id", str, &i);
            add_subst_time(lbs, slist, svalue, "entry time", date, &i);

            strsubst(preset, sizeof(preset), slist, svalue, i);
            if (strlen(preset) > NAME_LENGTH - 100) {
               if (strstr(preset + 100, "<br>")) {
                  strlcpy(str, strstr(preset + 100, "<br>"), sizeof(str));
               } else
                  strlcpy(str, preset + 100, sizeof(str));

               strcpy(preset, "...");
               strlcat(preset, str, sizeof(preset));
            }
            if (strncmp(preset, "<br>", 4) == 0)
               strcpy(attrib[index], preset + 4);
            else
               strcpy(attrib[index], preset);
         }
      }
   }

   /* header */

   if (getcfg(lbs->name, "Edit Page Title", str, sizeof(str))) {
      i = build_subst_list(lbs, (char (*)[NAME_LENGTH]) slist, (char (*)[NAME_LENGTH]) svalue, NULL, TRUE);
      strsubst(str, sizeof(str), (char (*)[NAME_LENGTH]) slist, (char (*)[NAME_LENGTH]) svalue, i);
      strip_html(str);
   } else
      sprintf(str, "ELOG %s", lbs->name);

   show_html_header(lbs, FALSE, str, FALSE, FALSE, cookie);

   /* java script for checking required attributes and to check for cancelled edits */
   rsprintf("<script type=\"text/javascript\">\n");
   rsprintf("<!--\n\n");
   rsprintf("var submitted = false;\n");

   if (breedit) {
      rsprintf("var modified = true;\n");
      rsprintf("window.status = \"%s\";\n", loc("Entry has been modified"));
   } else
      rsprintf("var modified = false;\n");

   rsprintf("\n");
   rsprintf("function chkform()\n");
   rsprintf("{\n");

   for (i = 0; i < n_attr; i++) {
      if ((attr_flags[i] & AF_REQUIRED)) {

         /* convert blanks to underscores */
         strcpy(ua, attr_list[i]);
         btou(ua);

         /* convert dots to underscores */
         dtou(ua);

         if (attr_flags[i] & AF_MULTI) {
            rsprintf("  if (\n");
            for (j = 0; j < MAX_N_LIST && attr_options[i][j][0]; j++) {
               sprintf(str, "%s_%d", ua, j);
               rsprintf("    !document.form1.%s.checked", str);
               if (attr_options[i][j + 1][0])
                  rsprintf(" &&\n");
            }
            rsprintf(") {\n");
            sprintf(str, loc("Please select at least one '%s'"), attr_list[i]);
            rsprintf("    alert(\"%s\");\n", str);
            rsprintf("    document.form1.%s_0.focus();\n", ua);
            rsprintf("    return false;\n");
            rsprintf("  }\n");

         } else if (attr_flags[i] & AF_RADIO) {
            rsprintf("  for (var i=0 ; i<document.form1.%s.length ; i++)\n", ua);
            rsprintf("    if (document.form1.%s[i].checked) { break }\n", ua);
            rsprintf("  if (i == document.form1.%s.length) {\n", ua);
            sprintf(str, loc("Please select a '%s'"), attr_list[i]);
            rsprintf("    alert(\"%s\");\n", str);
            rsprintf("    document.form1.%s[0].focus();\n", ua);
            rsprintf("    return false;\n");
            rsprintf("  }\n");

         } else if (attr_flags[i] & (AF_DATE | AF_DATETIME)) {
            rsprintf("  if (document.form1.m%d.value == \"\") {\n", i);
            sprintf(str, loc("Please enter month for attribute '%s'"), attr_list[i]);
            rsprintf("    alert(\"%s\");\n", str);
            rsprintf("    document.form1.m%d.focus();\n", i);
            rsprintf("    return false;\n");
            rsprintf("  }\n");
            rsprintf("  if (document.form1.d%d.value == \"\") {\n", i);
            sprintf(str, loc("Please enter day for attribute '%s'"), attr_list[i]);
            rsprintf("    alert(\"%s\");\n", str);
            rsprintf("    document.form1.d%d.focus();\n", i);
            rsprintf("    return false;\n");
            rsprintf("  }\n");
            rsprintf("  if (document.form1.y%d.value == \"\") {\n", i);
            sprintf(str, loc("Please enter year for attribute '%s'"), attr_list[i]);
            rsprintf("    alert(\"%s\");\n", str);
            rsprintf("    document.form1.y%d.focus();\n", i);
            rsprintf("    return false;\n");
            rsprintf("  }\n");

            if (attr_flags[i] & AF_DATETIME) {
               rsprintf("  if (document.form1.h%d.value == \"\") {\n", i);
               sprintf(str, loc("Please enter hour for attribute '%s'"), attr_list[i]);
               rsprintf("    alert(\"%s\");\n", str);
               rsprintf("    document.form1.h%d.focus();\n", i);
               rsprintf("    return false;\n");
               rsprintf("  }\n");
               rsprintf("  if (document.form1.n%d.value == \"\") {\n", i);
               sprintf(str, loc("Please enter minute for attribute '%s'"), attr_list[i]);
               rsprintf("    alert(\"%s\");\n", str);
               rsprintf("    document.form1.n%d.focus();\n", i);
               rsprintf("    return false;\n");
               rsprintf("  }\n");
               rsprintf("  if (document.form1.s%d.value == \"\") {\n", i);
               sprintf(str, loc("Please enter second for attribute '%s'"), attr_list[i]);
               rsprintf("    alert(\"%s\");\n", str);
               rsprintf("    document.form1.s%d.focus();\n", i);
               rsprintf("    return false;\n");
               rsprintf("  }\n");
            }

         } else {
            rsprintf("  if (document.form1.%s.value == \"\") {\n", ua);
            sprintf(str, loc("Please enter attribute '%s'"), attr_list[i]);
            rsprintf("    alert(\"%s\");\n", str);
            rsprintf("    document.form1.%s.focus();\n", ua);
            rsprintf("    return false;\n");
            rsprintf("  }\n");
         }
      }

      if (attr_flags[i] & AF_NUMERIC) {
         /* convert blanks to underscores */
         strcpy(ua, attr_list[i]);
         btou(ua);
         /* convert dots to underscores */
         dtou(ua);

         rsprintf("  for (var i=0 ; i<document.form1.%s.value.length ; i++)\n", ua);
         rsprintf("    if (document.form1.%s.value.charAt(i) != \",\" &&\n", ua);
         rsprintf("        document.form1.%s.value.charAt(i) != \".\" &&\n", ua);
         rsprintf("        document.form1.%s.value.charAt(i) != \"-\" &&\n", ua);
         rsprintf("        (document.form1.%s.value.charAt(i) < \"0\" ||\n", ua);
         rsprintf("         document.form1.%s.value.charAt(i) > \"9\")) { break }\n", ua);
         rsprintf("  if (i<document.form1.%s.value.length) {\n", ua);
         sprintf(str, loc("Please enter numeric value for '%s'"), attr_list[i]);
         rsprintf("    alert(\"%s\");\n", str);
         rsprintf("    document.form1.%s.focus();\n", ua);
         rsprintf("    return false;\n");
         rsprintf("  }\n");
      }

      if (attr_flags[i] & (AF_DATE | AF_DATETIME)) {
         rsprintf("  for (var i=0 ; i<document.form1.y%d.value.length ; i++)\n", i);
         rsprintf("    if ((document.form1.y%d.value.charAt(i) < \"0\" ||\n", i);
         rsprintf("         document.form1.y%d.value.charAt(i) > \"9\")) { break }\n", i);
         rsprintf("  if (i<document.form1.y%d.value.length) {\n", i);
         sprintf(str, loc("Please enter numeric value for year of attribute '%s'"), attr_list[i]);
         rsprintf("    alert(\"%s\");\n", str);
         rsprintf("    document.form1.y%d.focus();\n", i);
         rsprintf("    return false;\n");
         rsprintf("  }\n");
      }

   }

   rsprintf("  submitted = true;\n");
   rsprintf("  return true;\n");
   rsprintf("}\n\n");

   /* mark_submit() gets called via "Back" button */
   rsprintf("function mark_submit()\n");
   rsprintf("{\n");
   rsprintf("  submitted = true;\n");
   rsprintf("  return true;\n");
   rsprintf("}\n\n");

   /* chkupload() gets called via "Upload" button */
   rsprintf("function chkupload()\n");
   rsprintf("{\n");
   rsprintf("  if (document.form1.attfile.value == \"\") {\n");
   rsprintf("    alert(\"%s\");\n", loc("No attachment file specified"));
   rsprintf("    return false;\n");
   rsprintf("  }\n");
   rsprintf("  submitted = true;\n");
   rsprintf("  return true;\n");
   rsprintf("}\n\n");

   /* cond_submit() gets called via selection of new conditional attribute */
   rsprintf("function cond_submit()\n");
   rsprintf("{\n");
   rsprintf("  submitted = true;\n");
   rsprintf("  document.form1.submit();\n");
   rsprintf("}\n\n");

   /* abandon() gets called "onUnload" */
   rsprintf("function unload()\n");
   rsprintf("{\n");
   rsprintf("  if (!submitted && modified) {\n");
   rsprintf("    var subm = confirm(\"%s\");\n", loc("Submit modified ELOG entry?"));
   rsprintf("    if (subm) {\n");
   rsprintf("      document.form1.jcmd.value = \"%s\";\n", loc("Submit"));
   rsprintf("      document.form1.submit();\n");
   rsprintf("    } else {\n");
   rsprintf("      document.form1.jcmd.value = \"%s\";\n", loc("Back"));
   rsprintf("      document.form1.submit();\n");
   rsprintf("    }\n");
   rsprintf("  }\n");
   rsprintf("  if (!submitted && !modified) {\n");
   rsprintf("    document.form1.jcmd.value = \"%s\";\n", loc("Back"));
   rsprintf("    document.form1.submit();\n");
   rsprintf("    }\n");
   rsprintf("}\n\n");

   /* mod() gets called via throuch "onchange" event */
   rsprintf("function mod()\n");
   rsprintf("{\n");
   rsprintf("  modified = true;\n");
   rsprintf("  window.status = \"%s\";\n", loc("Entry has been modified"));
   rsprintf("}\n\n");

   /* switch_smileys turn on/off the smiley bar by setting the smcmd, which in turn
      sets the hsm=0/hsm=1 cookie */
   rsprintf("function switch_smileys()\n");
   rsprintf("{\n");
   if (show_smileys)
      rsprintf("   document.form1.smcmd.value = 'hsm';\n");
   else
      rsprintf("   document.form1.smcmd.value = 'ssm';\n");
   rsprintf("   document.form1.submit();\n");
   rsprintf("}\n\n");

   /* strings for elcode.js */
   rsprintf("linkText_prompt = '%s';\n", loc("Enter name of hypelink"));
   rsprintf("linkURL_prompt  = '%s';\n", loc("Enter URL of hypelink"));
   if (stristr(browser, "MSIE") && !stristr(browser, "opera"))
      rsprintf("browser = 'MSIE';\n");
   else if (stristr(browser, "Mozilla") && !stristr(browser, "opera") && !stristr(browser, "konqueror"))
      rsprintf("browser = 'Mozilla';\n");
   else
      rsprintf("browser = 'Other';\n");

   rsprintf("//-->\n");
   rsprintf("</script>\n");

   /* optionally load ELCode JavaScript code */
   if (enc_selected == 0)
      rsprintf("<script type=\"text/javascript\" src=\"../elcode.js\"></script>\n\n");

   /* external script if requested */
   if (isparam("js")) {
      rsprintf("<script src=\"%s\" type=\"text/javascript\">\n", getparam("js"));
      rsprintf("</script>\n\n");
   }

   rsprintf("</head>\n");

   script[0] = 0;
   if (isparam("inlineatt"))
      strcpy(script, " OnLoad=\"document.form1.Text.focus();\"");

   if (getcfg(lbs->name, "Use Lock", str, sizeof(str)) && atoi(str) == 1)
      rsprintf("<body onUnload=\"unload();\"%s>\n", script);
   else
      rsprintf("<body%s>\n", script);

   rsprintf("<form name=form1 method=\"POST\" action=\"./\" ");
   rsprintf("enctype=\"multipart/form-data\">\n");

   /*---- add password in case cookie expires during edit ----*/

   if (getcfg(lbs->name, "Write password", str, sizeof(str)))
      rsprintf("<input type=hidden name=\"wpwd\" value=\"%s\">\n", str);

   if (getcfg(lbs->name, "Password file", str, sizeof(str))) {
      rsprintf("<input type=hidden name=\"unm\" value=\"%s\">\n", getparam("unm"));
      rsprintf("<input type=hidden name=\"upwd\" value=\"%s\">\n", getparam("upwd"));
   }

   rsprintf("<input type=hidden name=\"jcmd\">\n");
   rsprintf("<input type=hidden name=\"smcmd\">\n");
   rsprintf("<input type=hidden name=\"inlineatt\">\n");

   /*---- title row ----*/

   show_standard_title(lbs->name, "", 0);

   /*---- menu buttons ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");

   /* default cmd */
   rsprintf("<input type=hidden name=cmd value=\"%s\">\n", loc("Update"));

   rsprintf
       ("<input type=\"submit\" name=\"cmd\" value=\"%s\" onClick=\"return chkform();\">\n", loc("Submit"));
   rsprintf
       ("<input type=\"submit\" name=\"cmd\" value=\"%s\" onClick=\"return chkform();\">\n", loc("Preview"));
   rsprintf
       ("<input type=\"submit\" name=\"cmd\" value=\"%s\" onClick=\"return mark_submit();\">\n", loc("Back"));
   rsprintf("</span></td></tr>\n\n");

   /*---- entry form ----*/

   /* table for two-column items */
   rsprintf("<tr><td><table class=\"listframe\" width=\"100%%\" cellspacing=0 cellpadding=0>");

   /* print required message if one of the attributes has it set */
   for (i = 0; i < n_attr; i++) {
      if (attr_flags[i] & AF_REQUIRED) {
         rsprintf
             ("<tr><td colspan=2 class=\"attribvalue\">%s <font color=red>*</font> %s</td></tr>\n",
              loc("Fields marked with"), loc("are required"));
         break;
      }
   }

   time(&now);
   if (bedit) {
      if (!getcfg(lbs->name, "Time format", format, sizeof(format)))
         strcpy(format, DEFAULT_TIME_FORMAT);

      ltime = date_to_ltime(date);
      pts = localtime(&ltime);
      my_strftime(str, sizeof(str), format, pts);
   } else {
      if (getcfg(lbs->name, "Time format", format, sizeof(format)))
         my_strftime(str, sizeof(str), format, localtime(&now));
      else
         strcpy(str, ctime(&now));
      strcpy(date, ctime(&now));
      date[24] = 0;
   }

   rsprintf("<tr><td nowrap width=\"10%%\" class=\"attribname\">%s:</td>", loc("Entry time"));
   rsprintf("<td class=\"attribvalue\">%s\n", str);
   rsprintf("<input type=hidden name=entry_date value=\"%s\"></td></tr>\n", date);

   if (_condition[0])
      rsprintf("<input type=hidden name=condition value=\"%s\"></td></tr>\n", _condition);

   /* retrieve attribute flags */
   for (i = 0; i < n_attr; i++) {
      format_flags[i] = 0;
      sprintf(str, "Format %s", attr_list[i]);
      if (getcfg(lbs->name, str, format, sizeof(format))) {
         n = strbreak(format, fl, 8, ",");
         if (n > 0)
            format_flags[i] = atoi(fl[0]);
      }
   }

   subtable = 0;

   /* generate list of attributes to show */
   if (getcfg(lbs->name, "Show attributes", str, sizeof(str))) {
      n_disp_attr = strbreak(str, list, MAX_N_ATTR, ",");
      for (i = 0; i < n_disp_attr; i++) {
         for (j = 0; j < n_attr; j++)
            if (strieq(attr_list[j], list[i]))
               break;
         if (!strieq(attr_list[j], list[i]))
            /* attribute not found */
            j = 0;
         attr_index[i] = j;
      }
   } else {
      for (i = 0; i < n_attr; i++)
         attr_index[i] = i;
      n_disp_attr = n_attr;
   }

   /* display attributes */
   for (aindex = 0; aindex < n_disp_attr; aindex++) {

      index = attr_index[aindex];

      strcpy(class_name, "attribname");
      strcpy(class_value, "attribvalue");
      input_size = 80;
      input_maxlen = NAME_LENGTH;
      strcpy(ua, attr_list[index]);
      btou(ua);
      dtou(ua);

      sprintf(str, "Format %s", attr_list[index]);
      if (getcfg(lbs->name, str, format, sizeof(format))) {
         n = strbreak(format, fl, 8, ",");
         if (n > 1)
            strlcpy(class_name, fl[1], sizeof(class_name));
         if (n > 2)
            strlcpy(class_value, fl[2], sizeof(class_value));
         if (n > 3 && atoi(fl[3]) > 0)
            input_size = atoi(fl[3]);
         if (n > 4 && atoi(fl[4]) > 0)
            input_maxlen = atoi(fl[4]);
      }

      if (format_flags[index] & AFF_SAME_LINE)
         /* if attribute on same line, do nothing */
         rsprintf("");
      else if (aindex < n_disp_attr - 1 && (format_flags[attr_index[aindex + 1]] & AFF_SAME_LINE)) {
         /* if next attribute on same line, start a new subtable */
         rsprintf("<tr><td colspan=2><table width=\"100%%\" cellpadding=0 cellspacing=0><tr>");
         subtable = 1;
      } else
         /* for normal attribute, start new row */
         rsprintf("<tr>");

      strcpy(star, (attr_flags[index] & AF_REQUIRED) ? "<font color=red>*</font>" : "");

      /* display text box with optional tooltip */
      sprintf(str, "Tooltip %s", attr_list[index]);
      title[0] = 0;
      if (getcfg(lbs->name, str, comment, sizeof(comment)))
         sprintf(title, " title=\"%s\"", comment);

      rsprintf("<td%s nowrap class=\"attribname\">", title);

      /* display attribute name */
      rsprintf("%s%s:", attr_list[index], star);

      /* show optional comment */
      sprintf(str, "Comment %s", attr_list[index]);
      if (getcfg(lbs->name, str, comment, sizeof(comment)))
         rsprintf("<br><span class=\"selcomment\"><b>%s</b></span>\n", comment);

      rsprintf("</td>\n");

      /* if attribute cannot be changed, just display text */
      if ((attr_flags[index] & AF_LOCKED) ||
          (bedit && (attr_flags[index] & AF_FIXED_EDIT)) || (message_id && !bedit
                                                             && (attr_flags[index] & AF_FIXED_REPLY))) {
         if (attr_flags[index] & AF_DATE) {

            if (!getcfg(lbs->name, "Date format", format, sizeof(format)))
               strcpy(format, DEFAULT_DATE_FORMAT);

            ltime = atoi(attrib[index]);
            pts = localtime(&ltime);
            if (ltime == 0)
               strcpy(str, "-");
            else
               my_strftime(str, sizeof(str), format, pts);

         } else
            strlcpy(str, attrib[index], sizeof(str));

         rsprintf("<td%s class=\"attribvalue\">\n", title);
         rsputs2(lbs, str);
         rsprintf("&nbsp;");

         if (attr_flags[index] & AF_MULTI) {
            for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++) {
               sprintf(str, "%s_%d", ua, i);

               if (strstr(attrib[index], attr_options[index][i]))
                  rsprintf("<input type=\"hidden\" name=\"%s\" value=\"%s\">\n", str, attr_options[index][i]);
            }
         } else if (attr_flags[index] & AF_ICON) {
            for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++) {
               sprintf(str, "%s_%d", ua, i);

               if (strstr(attrib[index], attr_options[index][i]))
                  rsprintf("<input type=\"hidden\" name=\"%s\" value=\"%s\">\n", str, attr_options[index][i]);
            }
         } else {
            strencode2(str, attrib[index]);
            rsprintf("<input type=\"hidden\" name=\"%s\" value=\"%s\"></td>\n", ua, str);
         }
      } else {
         if (attr_options[index][0][0] == 0) {

            if (attr_flags[index] & AF_DATE) {

               year = month = day = 0;
               if (attrib[index][0]) {
                  ltime = atoi(attrib[index]);
                  pts = localtime(&ltime);
                  year = pts->tm_year + 1900;
                  month = pts->tm_mon + 1;
                  day = pts->tm_mday;
               }

               rsprintf("<td%s class=\"attribvalue\">", title);
               sprintf(str, "%d", index);
               show_date_selector(day, month, year, str);
               rsprintf("</td>\n");

            } else if (attr_flags[index] & AF_DATETIME) {

               year = month = day = 0;
               hour = min = sec = -1;
               if (attrib[index][0]) {
                  ltime = atoi(attrib[index]);
                  pts = localtime(&ltime);
                  year = pts->tm_year + 1900;
                  month = pts->tm_mon + 1;
                  day = pts->tm_mday;
                  hour = pts->tm_hour;
                  min = pts->tm_min;
                  sec = pts->tm_sec;
               }

               rsprintf("<td%s class=\"attribvalue\">", title);
               sprintf(str, "%d", index);
               show_date_selector(day, month, year, str);
               rsprintf("&nbsp;&nbsp;");
               show_time_selector(hour, min, sec, str);

               rsprintf("</td>\n");

            } else if (attr_flags[index] & AF_USERLIST) {

               rsprintf("<td%s class=\"attribvalue\">\n", title);

               /* display drop-down box with list of users */
               rsprintf("<select name=\"%s\"", ua);
               rsprintf(" onChange=\"mod();\">\n");

               /* display emtpy option */
               rsprintf("<option value=\"\">- %s -\n", loc("please select"));

               for (i = 0;; i++) {
                  if (!enum_user_line(lbs, i, login_name, sizeof(login_name)))
                     break;
                  get_user_line(lbs, login_name, NULL, str, NULL, NULL, NULL);

                  if (strieq(str, attrib[index]))
                     rsprintf("<option selected value=\"%s\">%s\n", str, str);
                  else
                     rsprintf("<option value=\"%s\">%s\n", str, str);
               }

               rsprintf("</select>\n");

               rsprintf("</td>\n");

            } else {

               /* show normal edit field */
               rsprintf("<td%s class=\"attribvalue\">", title);

               strencode2(str, attrib[index]);
               rsprintf
                   ("<input type=\"text\" size=%d maxlength=%d name=\"%s\" value=\"%s\" onChange=\"mod();\">\n",
                    input_size, input_maxlen, ua, str);

               rsprintf("</td>\n");
            }

         } else {
            if (strieq(attr_options[index][0], "boolean")) {
               /* display checkbox */
               if (atoi(attrib[index]) == 1)
                  rsprintf
                      ("<td%s class=\"attribvalue\"><input type=checkbox checked name=\"%s\" value=1 onChange=\"mod();\">\n",
                       title, ua);
               else
                  rsprintf
                      ("<td%s class=\"attribvalue\"><input type=checkbox name=\"%s\" value=1 onChange=\"mod();\">\n",
                       title, ua);
            } else {

               sprintf(str, "extend_%d", index);
               if (isparam(str)) {

                  rsprintf("<td%s class=\"attribvalue\">\n", title);
                  rsprintf("<i>");
                  rsprintf(loc("Add new option here"), attr_list[index]);
                  rsprintf("&nbsp;:&nbsp;</i>\n");

                  if (attr_flags[index] & AF_MULTI)
                     rsprintf
                         ("<input type=\"text\" size=20 maxlength=%d name=\"%s_0\" value=\"%s\" onChange=\"mod();\">\n",
                          input_maxlen, ua, attrib[index]);
                  else
                     rsprintf
                         ("<input type=\"text\" size=20 maxlength=%d name=\"%s\" value=\"%s\" onChange=\"mod();\">\n",
                          input_maxlen, ua, attrib[index]);

                  rsprintf("<input type=\"hidden\" name=\"extend_%d\" value=\"1\">\n", index);
                  rsprintf("</td>\n");

               } else if (attr_flags[index] & AF_MULTI) {

                  /* display multiple check boxes */
                  rsprintf("<td%s nowrap class=\"attribvalue\">\n", title);

                  for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++) {
                     sprintf(str, "%s_%d", ua, i);

                     if (strstr(attrib[index], attr_options[index][i]))
                        rsprintf
                            ("<input type=checkbox id=\"%s\" name=\"%s\" value=\"%s\" checked onChange=\"mod();\">\n",
                             str, str, attr_options[index][i]);
                     else
                        rsprintf
                            ("<input type=checkbox id=\"%s\" name=\"%s\" value=\"%s\" onChange=\"mod();\">\n",
                             str, str, attr_options[index][i]);

                     rsprintf("<label for=\"%s\">%s</label>\n", str, attr_options[index][i]);

                     if (format_flags[index] & AFF_MULTI_LINE)
                        rsprintf("<br>");
                  }

                  if (attr_flags[index] & AF_EXTENDABLE) {
                     sprintf(str, loc("Add %s"), attr_list[index]);
                     rsprintf
                         ("<input type=submit name=\"extend_%d\" value=\"%s\" onClick=\"return mark_submit();\">\n",
                          index, str);
                  }

                  rsprintf("</td>\n");

               } else if (attr_flags[index] & AF_RADIO) {
                  /* display radio buttons */
                  rsprintf("<td%s class=\"attribvalue\">\n", title);
                  rsprintf("<table cellpadding=0 cellspacing=0><tr>\n");

                  for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++) {
                     strlcpy(str, attr_options[index][i], sizeof(str));
                     if (strchr(str, '{'))
                        *strchr(str, '{') = 0;

                     if (strstr(attrib[index], attr_options[index][i])
                        || strieq(str, attrib[index]))
                        rsprintf
                            ("<td nowrap><input type=radio id=\"%s\" name=\"%s\" value=\"%s\" checked onChange=\"mod();\">\n",
                             str, ua, str);
                     else
                        rsprintf
                            ("<td nowrap><input type=radio id=\"%s\" name=\"%s\" value=\"%s\" onChange=\"mod();\">\n",
                             str, ua, str);

                     rsprintf("<label for=\"%s\">%s</label>\n", str, str);

                     rsprintf("</td>\n");
                     if (format_flags[index] & AFF_MULTI_LINE)
                        rsprintf("<br>");
                  }
                  rsprintf("</tr></table>\n");

                  if (attr_flags[index] & AF_EXTENDABLE) {
                     sprintf(str, loc("Add %s"), attr_list[index]);
                     rsprintf
                         ("<input type=submit name=\"extend_%d\" value=\"%s\" onClick=\"return mark_submit();\">\n",
                          index, str);
                  }

                  rsprintf("</td>\n");

               } else if (attr_flags[index] & AF_ICON) {
                  /* display icons */
                  rsprintf("<td%s nowrap class=\"attribvalue\">\n", title);
                  rsprintf("<table cellpadding=0 cellspacing=0><tr>\n");

                  for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++) {
                     if (strstr(attrib[index], attr_options[index][i]))
                        rsprintf
                            ("<td nowrap><input type=radio checked name=\"%s\" value=\"%s\" onChange=\"mod();\">",
                             ua, attr_options[index][i]);
                     else
                        rsprintf
                            ("<td nowrap><input type=radio name=\"%s\" value=\"%s\" onChange=\"mod();\">",
                             ua, attr_options[index][i]);

                     sprintf(str, "Icon comment %s", attr_options[index][i]);
                     getcfg(lbs->name, str, comment, sizeof(comment));

                     if (comment[0])
                        rsprintf("<img src=\"icons/%s\" alt=\"%s\" title=\"%s\">\n",
                                 attr_options[index][i], comment, comment);
                     else
                        rsprintf("<img src=\"icons/%s\" alt=\"%s\" title=\"%s\">\n",
                                 attr_options[index][i], attr_options[index][i], attr_options[index][i]);

                     rsprintf("</td>\n");
                     if ((format_flags[index] & AFF_MULTI_LINE) && attr_options[index][i+1][0]) {
                        rsprintf("</tr><tr>\n");
                     }
                  }

                  rsprintf("</tr></table></td>\n");

               } else {

                  rsprintf("<td%s class=\"attribvalue\">\n", title);

                  /* display drop-down box */
                  rsprintf("<select name=\"%s\"", ua);

                  if (is_cond_attr(index))
                     rsprintf(" onChange=\"cond_submit()\">\n");
                  else
                     rsprintf(" onChange=\"mod();\">\n");

                  /* display emtpy option */
                  rsprintf("<option value=\"\">- %s -\n", loc("please select"));

                  for (i = 0; i < MAX_N_LIST && attr_options[index][i][0]; i++) {
                     strlcpy(str, attr_options[index][i], sizeof(str));
                     if (strchr(str, '{'))
                        *strchr(str, '{') = 0;

                     if (strieq(attr_options[index][i], attrib[index])
                         || strieq(str, attrib[index]))
                        rsprintf("<option selected value=\"%s\">%s\n", str, str);
                     else
                        rsprintf("<option value=\"%s\">%s\n", str, str);
                  }

                  rsprintf("</select>\n");

                  if (is_cond_attr(index)) {
                     /* show "update" button only of javascript is not enabled */
                     rsprintf("<noscript>\n");
                     rsprintf("<input type=submit value=\"%s\">\n", loc("Update"));
                     rsprintf("</noscript>\n");
                  }

                  if (attr_flags[index] & AF_EXTENDABLE) {
                     sprintf(str, loc("Add %s"), attr_list[index]);
                     rsprintf
                         ("<input type=submit name=\"extend_%d\" value=\"%s\" onClick=\"return mark_submit();\">\n",
                          index, str);
                  }

                  rsprintf("</td>\n");
               }
            }
         }
      }

      if (aindex < n_disp_attr - 1 && (format_flags[attr_index[aindex + 1]] & AFF_SAME_LINE) == 0) {
         /* if next attribute not on same line, close row or subtable */
         if (subtable) {
            rsprintf("</table></td></tr>\n");
            subtable = 0;
         } else
            rsprintf("</tr>");
      }

      /* if last attribute, close row or subtable */
      if (aindex == n_disp_attr - 1) {
         if (subtable) {
            rsprintf("</table></td></tr>\n");
            subtable = 0;
         } else
            rsprintf("</tr>");
      }
   }

   if (preview) {
      _current_message_id = message_id;
      rsprintf("<tr><td colspan=2 class=\"messageframe\">\n");

      if (strieq(encoding, "plain")) {
         rsputs("<pre class=\"messagepre\">");
         rsputs2(lbs, text);
         rsputs("</pre>");
      } else if (strieq(encoding, "ELCode"))
         rsputs_elcode(lbs, text);
      else
         rsputs(text);

      rsprintf("</td></tr>\n");
   }

   if (enc_selected == 0) {
      rsprintf("<tr><td colspan=2 class=\"toolframe\">\n");

      ricon("bold", loc("bold text"), "elcode(document.form1.Text, 'B','')");
      ricon("italic", loc("italics text"), "elcode(document.form1.Text, 'I','')");
      ricon("underline", loc("underlined text"), "elcode(document.form1.Text, 'U','')");

      rsprintf(" ");
      ricon("center", loc("centered text"), "elcode(document.form1.Text, 'CENTER','')");

      rsprintf(" ");
      ricon("url", loc("insert hyperlink"), "queryURL(document.form1.Text)");
      ricon("email", loc("insert email"), "elcode(document.form1.Text, 'EMAIL','')");

      sprintf(str, "window.open('upload.html', '',");
      strlcat(str, "'top=280,left=350,width=500,height=120,dependent=yes,", sizeof(str));
      strlcat(str, "menubar=no,status=no,scrollbars=no,location=no,resizable=yes')", sizeof(str));
      ricon("image", loc("insert image"), str);

      rsprintf(" ");
      ricon("quote", loc("insert quote"), "elcode(document.form1.Text, 'QUOTE','')");
      ricon("list", loc("insert list"), "elcode(document.form1.Text, 'LIST','')");

      rsprintf(" ");
      ricon("code", loc("insert code"), "elcode(document.form1.Text, 'CODE','')");

      if (show_smileys)
         rsprintf
             (" <img align=\"middle\" name=\"smileys\" src=\"icons/eld_smile.png\" alt=\"%s\" title=\"%s\" border=\"0\"",
              loc("hide the smiley bar"), loc("hide the smiley bar"));
      else
         rsprintf
             (" <img align=\"middle\" name=\"smileys\" src=\"icons/elc_smile.png\" alt=\"%s\" title=\"%s\" border=\"0\"",
              loc("show the smiley bar"), loc("show the smiley bar"));
      rsprintf(" onclick=\"switch_smileys()\"");
      rsprintf(" onmouseover=\"this.style.cursor='hand';\" />\n");

      rsprintf(" <select name=\"font\" ");
      rsprintf("onchange=\"elcode(document.form1.Text,'FONT',this.options[this.selectedIndex].value);");
      rsprintf("this.selectedIndex=0;\">\n");
      rsprintf("<option value=\"0\">%s</option>\n", loc("FONT"));
      rsprintf("<option value=\"arial\">Arial</option>\n");
      rsprintf("<option value=\"comic sans ms\">Comic Sans MS</option>\n");
      rsprintf("<option value=\"courier\">Courier New</option>\n");
      rsprintf("<option value=\"tahoma\">Tahoma</option>\n");
      rsprintf("<option value=\"times new roman\">Times New Roman</option>\n");
      rsprintf("<option value=\"verdana\">Verdana</option>\n");
      rsprintf("</select>\n");

      rsprintf(" <select name=\"size\" ");
      rsprintf("onchange=\"elcode(document.form1.Text,'SIZE',this.options[this.selectedIndex].value);");
      rsprintf("this.selectedIndex=0;\">\n");
      rsprintf("<option value=\"0\">%s</option>\n", loc("SIZE"));
      rsprintf("<option value=\"1\">1</option>\n");
      rsprintf("<option value=\"2\">2</option>\n");
      rsprintf("<option value=\"3\">3</option>\n");
      rsprintf("<option value=\"4\">4</option>\n");
      rsprintf("<option value=\"5\">5</option>\n");
      rsprintf("<option value=\"6\">6</option>\n");
      rsprintf("</select>\n");

      rsprintf(" <select name=\"color\" ");
      rsprintf("onchange=\"elcode(document.form1.Text,'COLOR',this.options[this.selectedIndex].value);");
      rsprintf("this.selectedIndex=0;\">\n");
      rsprintf("<option value=\"0\">%s</option>\n", loc("COLOR"));
      rsprintf("<option value=\"blue\" style=\"color:blue\">blue</option>\n");
      rsprintf("<option value=\"darkblue\" style=\"color:darkblue\">dark-blue</option>\n");
      rsprintf("<option value=\"orange\" style=\"color:orange\">orange</option>\n");
      rsprintf("<option value=\"red\" style=\"color:red\">red</option>\n");
      rsprintf("<option value=\"darkred\" style=\"color:darkred\">dark red</option>\n");
      rsprintf("<option value=\"green\" style=\"color:green\">green</option>\n");
      rsprintf("<option value=\"darkgreen\" style=\"color:darkgreen\">dark-green</option>\n");
      rsprintf("<option value=\"pink\" style=\"color:deeppink\">pink</option>\n");
      rsprintf("<option value=\"purple\" style=\"color:purple\">purple</option>\n");
      rsprintf("<option value=\"chocolate\" style=\"color:chocolate\">chocolate</option>\n");
      rsprintf("</select>");

      rsprintf("</td></tr>\n");
   }

   /* main box for text box and icons */
   rsprintf("<tr><td colspan=2 class=\"attribvalue\">\n");
   if (enc_selected == 0)
      rsprintf("<table border=\"0\"><tr>\n");

   if (enc_selected == 0 && show_smileys) {

      rsprintf("<td class=\"menuframe\">\n");
      rsicon("smile", loc("smiling"), ":)");
      rsprintf("<br />\n");
      rsicon("happy", loc("happy"), ":))");
      rsprintf("<br />\n");
      rsicon("wink", loc("winking"), ";)");
      rsprintf("<br />\n");
      rsicon("biggrin", loc("big grin"), ":D");
      rsprintf("<br />\n");
      rsicon("crying", loc("crying"), ";(");
      rsprintf("<br />\n");
      rsicon("cool", loc("cool"), "8)");
      rsprintf("<br />\n");
      rsicon("frown", loc("frowning"), ":(");
      rsprintf("<br />\n");
      rsicon("confused", loc("confused"), "?)");
      rsprintf("<br />\n");
      rsicon("eek", loc("eek"), "8o");
      rsprintf("<br />\n");
      rsicon("mad", loc("mad"), "X(");
      rsprintf("<br />\n");
      rsicon("pleased", loc("pleased"), ":]");
      rsprintf("<br />\n");
      rsicon("tongue", loc("tongue"), ":P");
      rsprintf("<br />\n");
      rsicon("yawn", loc("yawn"), ":O");

      rsprintf("</td>\n");
   }

   if (enc_selected == 0)
      rsprintf("<td class=\"attribvalue\">\n");

   /* set textarea width */
   width = 112;

   if (getcfg(lbs->name, "Message width", str, sizeof(str)))
      width = atoi(str);

   /* increased width according to longest line */
   if (message_id && enc_selected == 1) {
      p = text;
      do {
         pend = strchr(p, '\n');
         if (pend == NULL)
            pend = p + strlen(p);

         if ((int) pend - (int) p + 1 > width)
            width = (int) pend - (int) p + 1;

         if (*pend == 0)
            break;

         p = pend;
         while (*p && (*p == '\r' || *p == '\n'))
            p++;
      } while (1);


      /* leave space for '> ' */
      if (!bedit && !bduplicate)
         width += 2;
   }

   /* set textarea height */
   height = 20;
   if (getcfg(lbs->name, "Message height", str, sizeof(str)))
      height = atoi(str);

   if (breply)
      /* hidden text for original message */
      rsprintf("<input type=hidden name=reply_to value=\"%d\">\n", message_id);

   if (breedit || bupload)
      /* hidden text for original message */
      rsprintf("<input type=hidden name=reply_to value=\"%s\">\n", getparam("reply_to"));

   if (bedit && message_id)
      rsprintf("<input type=hidden name=edit_id value=\"%d\">\n", message_id);

   if (getcfg(lbs->name, "Message comment", comment, sizeof(comment))
       && !message_id) {
      rsputs(comment);
      rsputs("<br>\n");
   }

   if (getcfg(lbs->name, "Reply comment", comment, sizeof(comment))
       && breply) {
      rsputs(comment);
      rsputs("<br>\n");
   }

   preset_text = getcfg(lbs->name, "Preset text", str, sizeof(str));
   if (preset_text) {

      /* don't use preset text if editing */
      if (bedit || bduplicate)
         preset_text = FALSE;

      /* user preset on reedit only if preset is under condition */
      if (breedit && getcfg(lbs->name, "Preset text", str, sizeof(str)) == 2)
         preset_text = TRUE;
   }

   if (!getcfg(lbs->name, "Show text", str, sizeof(str))
       || atoi(str) == 1) {

      if (getcfg(lbs->name, "Fix text", str, sizeof(str)) && atoi(str) == 1)
         strcpy(str, " readonly");
      else
         strcpy(str, "");

      if (enc_selected == 1)
         /* use hard wrapping only for plain text */
         rsprintf("<textarea rows=%d cols=%d wrap=hard %s name=\"Text\" onChange=\"mod();\">",
                  height, width, str);
      else
         rsprintf("<textarea rows=%d cols=%d %s name=\"Text\" onChange=\"mod();\">", height, width, str);

      if (bedit) {
         if (!preset_text) {

            j = build_subst_list(lbs, slist, svalue, attrib, TRUE);
            sprintf(mid, "%d", message_id);
            add_subst_list(slist, svalue, "message id", mid, &j);
            add_subst_time(lbs, slist, svalue, "entry time", date, &j);

            if (!bupload)
               if (getcfg(lbs->name, "Prepend on edit", str, sizeof(str))) {
                  strsubst(str, sizeof(str), slist, svalue, j);
                  while (strstr(str, "\\n"))
                     memcpy(strstr(str, "\\n"), "\r\n", 2);
                  rsputs3(str);
               }

            /* use rsputs3 which just converts "<", ">", "&", "\"" to &gt; etc. */
            /* otherwise some HTML statments would break the page syntax        */
            rsputs3(text);

            if (!bupload)
               if (getcfg(lbs->name, "Append on edit", str, sizeof(str))) {
                  strsubst(str, sizeof(str), slist, svalue, j);
                  while (strstr(str, "\\n"))
                     memcpy(strstr(str, "\\n"), "\r\n", 2);
                  rsputs3(str);
               }
         }
      } else if (bduplicate) {

         rsputs3(text);

      } else if (breply) {
         if (!getcfg(lbs->name, "Quote on reply", str, sizeof(str))
             || atoi(str) > 0) {
            if (getcfg(lbs->name, "Prepend on reply", str, sizeof(str))) {
               j = build_subst_list(lbs, slist, svalue, attrib, TRUE);
               sprintf(mid, "%d", message_id);
               add_subst_list(slist, svalue, "message id", mid, &j);
               add_subst_time(lbs, slist, svalue, "entry time", date, &j);

               strsubst(str, sizeof(str), slist, svalue, j);
               while (strstr(str, "\\n"))
                  memcpy(strstr(str, "\\n"), "\r\n", 2);
               rsputs3(str);
            }

            p = text;

            if (text[0]) {
               if (!getcfg(lbs->name, "Reply string", reply_string, sizeof(reply_string)))
                  strcpy(reply_string, "> ");

               if (enc_selected == 0) {

                  /* check for author */
                  if (orig_author[0])
                     rsprintf("[quote=\"%s\"]", orig_author);
                  else
                     rsprintf("[quote]");
                  rsputs3(text);
                  rsprintf("[/quote]\r\n");
               } else {
                  do {
                     if (strchr(p, '\n')) {
                        *strchr(p, '\n') = 0;

                        if (encoding[0] == 'H') {
                           rsputs3(reply_string);
                           rsprintf("%s<br>\n", p);
                        } else {
                           rsputs(reply_string);
                           rsputs3(p);
                           rsprintf("\n");
                        }

                        p += strlen(p) + 1;
                        if (*p == '\n')
                           p++;
                     } else {
                        if (encoding[0] == 'H') {
                           rsputs3(reply_string);
                           rsprintf("%s<p>\n", p);
                        } else {
                           rsputs(reply_string);
                           rsputs3(p);
                           rsprintf("\n\n");
                        }

                        break;
                     }

                  } while (TRUE);
               }
            }

            if (getcfg(lbs->name, "Append on reply", str, sizeof(str))) {

               j = build_subst_list(lbs, slist, svalue, attrib, TRUE);
               sprintf(mid, "%d", message_id);
               add_subst_list(slist, svalue, "message id", mid, &j);
               add_subst_time(lbs, slist, svalue, "entry time", date, &j);
               strsubst(str, sizeof(str), slist, svalue, j);
               while (strstr(str, "\\n"))
                  memcpy(strstr(str, "\\n"), "\r\n", 2);
               rsputs3(str);
            }
         }
      }


      if (preset_text) {
         getcfg(lbs->name, "Preset text", str, sizeof(str));

         /* check if file starts with an absolute directory */
         if (str[0] == DIR_SEPARATOR || str[1] == ':')
            strcpy(file_name, str);
         else {
            strlcpy(file_name, resource_dir, sizeof(file_name));
            strlcat(file_name, str, sizeof(file_name));
         }

         /* check if file exists */
         fh = open(file_name, O_RDONLY | O_BINARY);
         if (fh > 0) {
            length = lseek(fh, 0, SEEK_END);
            lseek(fh, 0, SEEK_SET);
            buffer = xmalloc(length + 1);
            read(fh, buffer, length);
            buffer[length] = 0;
            close(fh);
            rsputs3(buffer);
            xfree(buffer);
         } else {
            j = build_subst_list(lbs, slist, svalue, attrib, TRUE);
            strsubst(str, sizeof(str), slist, svalue, j);
            while (strstr(str, "\\n"))
               memcpy(strstr(str, "\\n"), "\r\n", 2);
            rsputs3(str);
         }
      }

      rsprintf("</textarea><br>\n");

      /* Encoding radio buttons */

      rsprintf("<b>%s</b>: ", loc("Encoding"));

      if (enc_selected == 0)
         rsprintf("<input type=radio id=\"ELCode\" name=\"encoding\" value=\"ELCode\" checked>");
      else
         rsprintf
             ("<input type=radio id=\"ELCode\" name=\"encoding\" value=\"ELCode\" onclick=\"document.form1.submit()\">");
      rsprintf
          ("<label for=\"ELCode\"><a target=\"_blank\" href=\"?cmd=HelpELCode\">ELCode</a>&nbsp;&nbsp;</label>\n");

      if (enc_selected == 1)
         rsprintf("<input type=radio id=\"plain\" name=\"encoding\" value=\"plain\" checked>");
      else
         rsprintf
             ("<input type=radio id=\"plain\" name=\"encoding\" value=\"plain\" onclick=\"document.form1.submit()\">");
      rsprintf("<label for=\"plain\">plain&nbsp;&nbsp;</label>\n");

      if (enc_selected == 2)
         rsprintf("<input type=radio id=\"HTML\" name=\"encoding\" value=\"HTML\" checked>");
      else
         rsprintf
             ("<input type=radio id=\"HTML\" name=\"encoding\" value=\"HTML\" onclick=\"document.form1.submit()\">");
      rsprintf("<label for=\"HTML\">HTML&nbsp;&nbsp;</label>\n");

      rsprintf("<br>\n");
   }

   /* Suppress email check box */
   if (!(bedit && !breedit && !bupload && getcfg(lbs->name, "Suppress Email on edit", str, sizeof(str))
         && atoi(str) == 1)) {
      if (getcfg(lbs->name, "Suppress default", str, sizeof(str))) {
         if (atoi(str) == 0) {
            rsprintf("<input type=checkbox name=suppress value=1>%s\n", loc("Suppress Email notification"));
         } else if (atoi(str) == 1) {
            rsprintf("<input type=checkbox checked name=suppress value=1>%s\n",
                     loc("Suppress Email notification"));
         }

      } else {
         rsprintf("<input type=checkbox name=suppress value=1>%s\n", loc("Suppress Email notification"));
      }
   }

   /* Suppress execute shell check box */
   if (!bedit && getcfg(lbs->name, "Execute new", str, sizeof(str))) {
      if (getcfg(lbs->name, "Suppress execute default", str, sizeof(str))) {
         if (atoi(str) == 0) {
            rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
            rsprintf("<input type=checkbox name=shell_suppress value=1>%s\n",
                     loc("Suppress shell execution"));
         } else if (atoi(str) == 1) {
            rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
            rsprintf("<input type=checkbox checked name=shell_suppress value=1>%s\n",
                     loc("Suppress shell execution"));
         }
      } else {
         rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
         rsprintf("<input type=checkbox name=shell_suppress value=1>%s\n", loc("Suppress shell execution"));
      }
   }

   if (bedit && getcfg(lbs->name, "Execute edit", str, sizeof(str))) {
      if (getcfg(lbs->name, "Suppress execute default", str, sizeof(str))) {
         if (atoi(str) == 0) {
            rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
            rsprintf("<input type=checkbox name=shell_suppress value=1>%s\n",
                     loc("Suppress shell execution"));
         } else if (atoi(str) == 1) {
            rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
            rsprintf("<input type=checkbox checked name=shell_suppress value=1>%s\n",
                     loc("Suppress shell execution"));
         }
      } else {
         rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
         rsprintf("<input type=checkbox name=shell_suppress value=1>%s\n", loc("Suppress shell execution"));
      }
   }

   /* Resubmit check box */
   if (bedit && message_id) {
      if (getcfg(lbs->name, "Resubmit default", str, sizeof(str))) {
         if (atoi(str) == 0) {
            rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
            rsprintf("<input type=checkbox name=resubmit value=1>%s\n", loc("Resubmit as new entry"));
         } else if (atoi(str) == 1) {
            rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
            rsprintf("<input type=checkbox checked name=resubmit value=1>%s\n", loc("Resubmit as new entry"));
         }
      } else {
         rsprintf("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\n");
         rsprintf("<input type=checkbox name=resubmit value=1>%s\n", loc("Resubmit as new entry"));
      }
   }

   rsprintf("</tr>\n");

   if (enc_selected == 0)
      rsprintf("</table></td></tr>\n");

   for (i = 0; i < MAX_ATTACHMENTS; i++)
      if (!att[i][0]) {
         /* put first free attachment for show_uploader_finished() */
         rsprintf("<tr><td><input type=hidden name=\"next_attachment\" value=\"%d\"></td></tr>\n", i + 1);
         break;
      }

   if (!getcfg(lbs->name, "Enable attachments", str, sizeof(str))
       || atoi(str) > 0) {
      i = 0;
      if (bedit || bduplicate) {
         /* show existing attachments */
         for (i = 0; i < MAX_ATTACHMENTS; i++)
            if (att[i][0]) {
               rsprintf("<tr><td nowrap class=\"attribname\">%s %d:</td>\n", loc("Attachment"), i + 1);
               sprintf(str, "attachment%d", i);
               rsprintf("<td class=\"attribvalue\">\n");
               rsprintf("<input type=hidden name=\"%s\" value=\"%s\">\n", str, att[i]);
               rsprintf("%s\n", att[i] + 14);
               rsprintf
                   ("&nbsp;&nbsp;<input type=\"submit\" name=\"delatt%d\" value=\"%s\" onClick=\"return mark_submit();\"></td></tr>\n",
                    i, loc("Delete"));
            } else
               break;
      }

      /* optional attachment comment */
      if (getcfg(lbs->name, "Attachment comment", comment, sizeof(comment))
          && !message_id) {
         rsprintf("<tr><td colspan=2 class=\"attribvalue\">\n");
         rsputs(comment);
         rsputs("</td></tr>\n");
      }

      /* field for add attachment */
      if (att[MAX_ATTACHMENTS - 1][0]) {
         rsprintf("<tr><td colspan=2 class=\"attribname\">%s</td>\n",
                  loc("Maximum number of attachments reached"));
         rsprintf("</td></tr>\n");
      } else {
         rsprintf("<tr><td nowrap class=\"attribname\">%s %d:</td>\n", loc("Attachment"), i + 1);
         rsprintf
             ("<td class=\"attribvalue\"><input type=\"file\" size=\"60\" maxlength=\"200\" name=\"attfile\" accept=\"filetype/*\">\n");
         rsprintf
             ("&nbsp;&nbsp;<input type=\"submit\" name=\"cmd\" value=\"%s\" onClick=\"return chkupload();\">\n",
              loc("Upload"));
         rsprintf("</td></tr>\n");
      }
   }

   rsprintf("</table><!-- listframe -->\n");

   /*---- menu buttons again ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");
   rsprintf
       ("<input type=\"submit\" name=\"cmd\" value=\"%s\" onClick=\"return chkform();\">\n", loc("Submit"));
   rsprintf
       ("<input type=\"submit\" name=\"cmd\" value=\"%s\" onClick=\"return chkform();\">\n", loc("Preview"));
   rsprintf
       ("<input type=\"submit\" name=\"cmd\" value=\"%s\" onClick=\"return mark_submit();\">\n", loc("Back"));
   rsprintf("</span></td></tr>\n\n");

   rsprintf("</table><!-- show_standard_title -->\n");
   show_bottom_text(lbs);
   rsprintf("</form></body></html>\r\n");

   /* rescan unconditional attributes */
   if (_condition[0])
      scan_attributes(lbs->name);

   xfree(text);
}

/*------------------------------------------------------------------*/

void show_find_form(LOGBOOK * lbs)
{
   int i, j;
   char str[NAME_LENGTH], mode[NAME_LENGTH], comment[NAME_LENGTH], option[NAME_LENGTH];

   /*---- header ----*/

   show_standard_header(lbs, FALSE, loc("ELOG find"), NULL, FALSE, NULL);

   /*---- title ----*/

   show_standard_title(lbs->name, "", 0);

   /*---- menu buttons ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");

   rsprintf("<input type=submit value=\"%s\">\n", loc("Search"));
   rsprintf("<input type=reset value=\"%s\">\n", loc("Reset Form"));
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Back"));
   rsprintf("</span></td></tr>\n\n");

   /*---- entry form ----*/

   rsprintf("<tr><td class=\"form1\">\n");

   rsprintf("<b>%s:</b>&nbsp;&nbsp;", loc("Mode"));

   if (!getcfg(lbs->name, "Display mode", mode, sizeof(mode)))
      strcpy(mode, "Full");

   if (!getcfg(lbs->name, "Show text", str, sizeof(str)) || atoi(str) == 1) {
      if (strieq(mode, "Full"))
         rsprintf("<input type=radio id=\"full\" name=\"mode\" value=\"full\" checked>");
      else
         rsprintf("<input type=radio id=\"full\" name=\"mode\" value=\"full\">");
      rsprintf("<label for=\"full\">%s&nbsp;&nbsp;</label>\n", loc("Display full entries"));

      if (strieq(mode, "Summary"))
         rsprintf("<input type=radio id=\"summary\" name=\"mode\" value=\"summary\" checked>");
      else
         rsprintf("<input type=radio id=\"summary\" name=\"mode\" value=\"summary\">");
      rsprintf("<label for=\"summary\">%s&nbsp;&nbsp;</label>\n", loc("Summary only"));


   } else {
      if (strieq(mode, "Full") || strieq(mode, "Summary"))
         rsprintf("<input type=radio id=\"summary\" name=\"mode\" value=\"summary\" checked>");
      else
         rsprintf("<input type=radio id=\"summary\" name=\"mode\" value=\"summary\">");
      rsprintf("<label for=\"summary\">%s&nbsp;&nbsp;</label>\n", loc("Summary"));
   }

   if (strieq(mode, "Threaded"))
      rsprintf("<input type=radio id=\"threaded\" name=\"mode\" value=\"threaded\" checked>");
   else
      rsprintf("<input type=radio id=\"threaded\" name=\"mode\" value=\"threaded\">");
   rsprintf("<label for=\"threaded\">%s&nbsp;&nbsp;</label>\n", loc("Display threads"));

   if (strieq(mode, "CSV1"))
      rsprintf("<input type=radio id=\"CSV1\" name=\"mode\" value=\"CSV1\" checked>");
   else
      rsprintf("<input type=radio id=\"CSV1\" name=\"mode\" value=\"CSV1\">");
   rsprintf("<label for=\"CSV1\">%s&nbsp;&nbsp;</label>\n", loc("CSV (\",\" separated)"));

   if (strieq(mode, "CSV2"))
      rsprintf("<input type=radio id=\"CSV2\" name=\"mode\" value=\"CSV2\" checked>");
   else
      rsprintf("<input type=radio id=\"CSV2\" name=\"mode\" value=\"CSV2\">");
   rsprintf("<label for=\"CSV2\">%s&nbsp;&nbsp;</label>\n", loc("CSV (\";\" separated)"));

   if (strieq(mode, "XML"))
      rsprintf("<input type=radio id=\"XML\" name=\"mode\" value=\"XML\" checked>");
   else
      rsprintf("<input type=radio id=\"XML\" name=\"mode\" value=\"XML\">");
   rsprintf("<label for=\"XML\">XML&nbsp;&nbsp;</label>\n");

   if (strieq(mode, "Raw"))
      rsprintf("<input type=radio id=\"Raw\" name=\"mode\" value=\"Raw\" checked>");
   else
      rsprintf("<input type=radio id=\"Raw\" name=\"mode\" value=\"Raw\">");
   rsprintf("<label for=\"Raw\">Raw&nbsp;&nbsp;</label>\n");

   rsprintf("</td></tr>\n");

   rsprintf("<tr><td class=\"form2\"><b>%s:</b><br>", loc("Options"));

   rsprintf("<input type=checkbox id=\"attach\" name=\"attach\" value=1>");
   rsprintf("<label for=\"attach\">%s<br></label>\n", loc("Show attachments"));

   rsprintf("<input type=checkbox id=\"printable\" name=\"printable\" value=1>");
   rsprintf("<label for=\"printable\">%s<br></label>\n", loc("Printable output"));

   /* put hidden reverse=0, which gets used if the reverse checkbox is unchecked and "reverse sort=1" is in elogd.cfg */
   rsprintf("<input type=hidden name=\"reverse\" value=0>\n");
   if (getcfg(lbs->name, "Reverse sort", str, sizeof(str)) && atoi(str) == 1)
      rsprintf("<input type=checkbox id=\"reverse\" name=\"reverse\" value=1 checked>");
   else
      rsprintf("<input type=checkbox id=\"reverse\" name=\"reverse\" value=1>");
   rsprintf("<label for=\"reverse\">%s<br></label>\n", loc("Sort in reverse order"));

   /* count logbooks */
   for (i = 0;; i++) {
      if (!enumgrp(i, str))
         break;

      if (is_logbook(str))
         continue;
   }

   if (i > 2) {
      if (!getcfg(lbs->name, "Search all logbooks", str, sizeof(str)) || atoi(str) == 1) {
         rsprintf("<input type=checkbox id=all name=all value=1>\n");
         rsprintf("<label for=\"all\">%s</label><br>\n", loc("Search all logbooks"));
      }
   }

   rsprintf(loc("Display"));
   if (!getcfg(lbs->name, "Entries per page", str, sizeof(str)))
      strcpy(str, "20");
   rsprintf(" <input type=text name=npp size=3 value=%s> ", str);
   rsprintf(loc("entries per page"));

   rsprintf("</td></tr>\n");

   rsprintf("<tr><td class=\"form2\"><b>%s:</b>", loc("Filters"));
   sprintf(str, "<a href=\"http://dmoz.org/Computers/Programming/Languages/Regular_Expressions/\">");
   strcat(str, loc("regular expressions"));
   strcat(str, "</a>");
   rsprintf("&nbsp;&nbsp;<span class=\"selcomment\">(");
   rsprintf(loc("Text fields are treated as %s"), str);
   rsprintf(")</span><br>");

   /* table for two-column items */
   rsprintf("<table width=\"100%%\" cellspacing=0>\n");

   rsprintf("<tr><td class=\"attribname\" nowrap width=\"10%%\">%s:</td>", loc("Entry date"));
   rsprintf("<td class=\"attribvalue\"><table width=\"100%%\" cellspacing=0 border=0>\n");
   rsprintf("<tr><td width=\"1%%\">%s:<td>", loc("Start"));

   show_date_selector(0, 0, 0, "a");

   rsprintf("&nbsp;&nbsp;/&nbsp;&nbsp;%s:&nbsp;", loc("Show last"));

   rsprintf("<select name=last>\n");
   rsprintf("<option value=\"\">\n");
   rsprintf("<option value=1>%s\n", loc("Day"));
   rsprintf("<option value=7>%s\n", loc("Week"));
   rsprintf("<option value=31>%s\n", loc("Month"));
   rsprintf("<option value=92>%s\n", loc("3 Months"));
   rsprintf("<option value=182>%s\n", loc("6 Months"));
   rsprintf("<option value=364>%s\n", loc("Year"));
   rsprintf("</select> \n");

   rsprintf("</td></tr>\n");

   rsprintf("<tr><td width=\"1%%\">%s:<td>", loc("End"));

   show_date_selector(0, 0, 0, "b");

   rsprintf("</td></tr></table></td></tr>\n");

   for (i = 0; i < lbs->n_attr; i++) {
      rsprintf("<tr><td class=\"attribname\" nowrap>%s:</td>", attr_list[i]);
      rsprintf("<td class=\"attribvalue\">");
      if (attr_options[i][0][0] == 0) {

         if (attr_flags[i] & (AF_DATE | AF_DATETIME)) {

            rsprintf("<table width=\"100%%\" cellspacing=0 border=0>\n");
            rsprintf("<tr><td width=\"1%%\">%s:<td>", loc("Start"));
            sprintf(str, "%da", i);
            show_date_selector(0, 0, 0, str);
            if (attr_flags[i] & AF_DATETIME) {
               rsprintf("&nbsp;&nbsp;");
               show_time_selector(-1, -1, -1, str);
            }

            rsprintf("</td></tr>\n");
            rsprintf("<tr><td width=\"1%%\">%s:<td>", loc("End"));
            sprintf(str, "%db", i);
            show_date_selector(0, 0, 0, str);
            if (attr_flags[i] & AF_DATETIME) {
               rsprintf("&nbsp;&nbsp;");
               show_time_selector(-1, -1, -1, str);
            }

            rsprintf("</td></tr></table>\n");

         } else {

            rsprintf("<input type=\"text\" size=\"30\" maxlength=\"80\" name=\"%s\">\n", attr_list[i]);
         }

      } else {
         if (strieq(attr_options[i][0], "boolean"))
            rsprintf("<input type=checkbox name=\"%s\" value=1>\n", attr_list[i]);

         /* display image for icon */
         else if (attr_flags[i] & AF_ICON) {
            for (j = 0; j < MAX_N_LIST && attr_options[i][j][0]; j++) {
               strcpy(option, attr_options[i][j]);
               if (strchr(option, '{'))
                  *strchr(option, '{') = 0;

               sprintf(str, "Icon comment %s", option);
               getcfg(lbs->name, str, comment, sizeof(comment));

               rsprintf("<nobr><input type=radio name=\"%s\" value=\"%s\">", attr_list[i], option);

               if (comment[0] == 0)
                  strcpy(comment, option);
               rsprintf("<img src=\"icons/%s\" alt=\"%s\" title=\"%s\"></nobr>\n", option, comment, comment);
            }
         }

         /* display check boxes (or'ed) */
         else if (attr_flags[i] & AF_MULTI) {
            for (j = 0; j < MAX_N_LIST && attr_options[i][j][0]; j++) {
               sprintf(str, "%s_%d", attr_list[i], j);

               rsprintf("<nobr><input type=checkbox id=\"%s\" name=\"%s\" value=\"%s\"\">\n",
                        str, str, attr_options[i][j]);

               rsprintf("<label for=\"%s\">%s</label></nobr>\n", str, attr_options[i][j]);
            }
         }

         else {
            rsprintf("<select name=\"%s\">\n", attr_list[i]);
            rsprintf("<option value=\"\">\n");
            for (j = 0; j < MAX_N_LIST && attr_options[i][j][0]; j++) {

               strcpy(option, attr_options[i][j]);
               if (strchr(option, '{'))
                  *strchr(option, '{') = 0;

               rsprintf("<option value=\"%s\">%s\n", option, option);
            }
            rsprintf("</select>\n");
         }
      }
      rsprintf("</td></tr>\n");
   }

   rsprintf("<tr><td class=\"attribname\">%s:</td>", loc("Text"));
   rsprintf
       ("<td class=\"attribvalue\"><input type=\"text\" size=\"30\" maxlength=\"80\" name=\"subtext\">\n");

   rsprintf("<tr><td><td class=\"attribvalue\">\n");
   rsprintf("<input type=checkbox id=\"sall\" name=\"sall\" value=1>\n");
   rsprintf("<label for=\"sall\">%s</label>\n", loc("Search text also in attributes"));

   if (getcfg(lbs->name, "Case sensitive search", str, sizeof(str)) && atoi(str))
      rsprintf("<input type=checkbox id=\"sall\" name=\"casesensitive\" value=1 checked>\n");
   else
      rsprintf("<input type=checkbox id=\"sall\" name=\"casesensitive\" value=1>\n");
   rsprintf("<label for=\"casesensitive\">%s</label>\n", loc("Case sensitive"));

   rsprintf("</td></tr></table></td></tr></table>\n");
   show_bottom_text(lbs);
   rsprintf("</form></body></html>\r\n");
}

/*------------------------------------------------------------------*/

const char *find_section(const char *buf, const char *name)
{
   const char *pstart;
   char *pstr, str[80];

   do {
      if (*buf == '[') {
         pstart = buf;
         buf++;
         pstr = str;
         while (*buf && *buf != ']' && *buf != '\n' && *buf != '\r')
            *pstr++ = *buf++;
         *pstr = 0;
         if (strieq(str, name))
            return pstart;
      }

      if (buf)
         buf = strchr(buf, '\n');
      if (buf)
         buf++;
      if (buf && *buf == '\r')
         buf++;

   } while (buf);

   return NULL;
}

/*------------------------------------------------------------------*/

const char *find_next_section(const char *buf)
{
   do {
      if (*buf == '[')
         return buf;

      if (buf)
         buf = strchr(buf, '\n');
      if (buf)
         buf++;
   } while (buf);

   return NULL;
}

/*------------------------------------------------------------------*/

void load_config_section(char *section, char **buffer, char *error)
{
   int fh, length;
   char *p;

   error[0] = 0;
   *buffer = NULL;
   fh = open(config_file, O_RDONLY | O_BINARY);
   if (fh < 0) {
      sprintf(error, "Cannot read configuration file <b>\"%s\"</b>", config_file);
      return;
   }
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   *buffer = xmalloc(length + 1);
   read(fh, *buffer, length);
   (*buffer)[length] = 0;
   close(fh);

   if (section == NULL)
      return;

   if ((p = (char *) find_section(*buffer, section)) != NULL) {
      if (strchr(p, ']')) {
         p = strchr(p, ']') + 1;
         while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
            p++;
      }
      strlcpy(*buffer, p, length);
      if ((p = (char *) find_next_section(*buffer + 1)) != NULL) {
         *p = 0;

         /* strip trailing newlines */
         if (p) {
            p--;

            while (p > *buffer && (*p == '\n' || *p == '\r' || *p == ' ' || *p == '\t'))
               *p-- = 0;
         }
      } else {
         p = *buffer + strlen(*buffer) - 1;

         while (p > *buffer && (*p == '\n' || *p == '\r' || *p == ' ' || *p == '\t'))
            *p-- = 0;
      }
   }
}

/*------------------------------------------------------------------*/

void show_admin_page(LOGBOOK * lbs, char *top_group)
{
   int rows, cols;
   char *buffer, error_str[256];
   char section[NAME_LENGTH], str[NAME_LENGTH], grp[NAME_LENGTH];

   /*---- header ----*/

   sprintf(str, "ELOG %s", loc("Admin"));
   show_html_header(lbs, FALSE, str, TRUE, FALSE, NULL);

   rsprintf("<body><form method=\"POST\" action=\"./\" enctype=\"multipart/form-data\">\n");

   /*---- title ----*/

   show_standard_title(lbs->name, "", 0);

   /*---- menu buttons ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Save"));
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Cancel"));

   if (lbs->top_group[0] && (!top_group || strieq(top_group, "global"))) {
      if (is_admin_user("global", getparam("unm"))) {
         if (lbs->top_group[0]) {

            sprintf(str, "global %s", lbs->top_group);

            if (is_group(str)) {
               sprintf(grp, "[global %s]", lbs->top_group);
               sprintf(str, loc("Change %s"), grp);
               rsprintf("<input type=submit name=cmd value=\"%s\">\n", str);
            }
         }
      }
   }

   if (is_group("global") && !strieq(top_group, "global")) {
      if (is_admin_user_global(getparam("unm"))) {
         sprintf(str, loc("Change %s"), "[global]");
         rsprintf("<input type=submit name=cmd value=\"%s\">\n", str);
      }
   }

   if (top_group) {
      if (strieq(top_group, "global")) {
         rsprintf("<input type=hidden name=global value=\"global\">\n");
         strcpy(str, "[global]");
      } else {
         rsprintf("<input type=hidden name=global value=\"%s\">\n", top_group);
         sprintf(str, "[global %s]", top_group);
      }
      rsprintf("<br><center><b>%s</b></center>", str);
   }

   if (is_group("global") && !strieq(top_group, "global")) {
      if (is_admin_user("global", getparam("unm"))) {
         rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Delete this logbook"));
         rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Rename this logbook"));
         rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Create new logbook"));
      }
   }

   rsprintf("</span></td></tr>\n\n");

   /*---- entry form ----*/

   rsprintf("<tr><td class=\"form1\">\n");

   /* extract section of current logbook */
   if (top_group) {
      if (strieq(top_group, "global"))
         strcpy(section, "global");
      else
         sprintf(section, "global %s", top_group);
   } else
      strcpy(section, lbs->name);

   load_config_section(section, &buffer, error_str);

   if (error_str[0]) {
      rsprintf("</table></td></tr></table>\n");
      rsprintf("</body></html>\r\n");
      return;
   }

   if (getcfg(section, "Admin textarea", str, sizeof(str))
       && strchr(str, ',') != NULL) {
      cols = atoi(str);
      rows = atoi(strchr(str, ',') + 1);
   } else {
      cols = 120;
      rows = 30;
   }

   rsprintf("<textarea cols=%d rows=%d wrap=virtual name=Text>", cols, rows);

   rsputs3(buffer);
   xfree(buffer);

   rsprintf("</textarea>\n");

   /* put link for config page */
   rsprintf("<br><a target=\"_blank\" href=\"http://midas.psi.ch/elog/config.html\">Syntax Help</a>");

   rsprintf("</td></tr>\n");

   /*---- menu buttons ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");

   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Save"));
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Cancel"));
   rsprintf("</span></td></tr>\n\n");

   rsprintf("</table>\n\n");
   show_bottom_text(lbs);
   rsprintf("</form></body></html>\r\n");
}

/*------------------------------------------------------------------*/

void remove_crlf(char *buffer)
{
   char *p;

   /* convert \r\n -> \n */
   p = buffer;
   while ((p = strstr(p, "\r\n")) != NULL) {
      strcpy(p, p + 1);
   }
}

/*------------------------------------------------------------------*/

void adjust_crlf(char *buffer, int bufsize)
{
   char *p;

#ifdef OS_UNIX

   /* convert \r\n -> \n */
   bufsize = 0;                 // avoid compiler warning about unused bufsize
   p = buffer;
   while ((p = strstr(p, "\r\n")) != NULL) {
      strcpy(p, p + 1);
   }
#else

   char *tmpbuf;

   assert(bufsize);
   tmpbuf = xmalloc(bufsize);

   /* convert \n -> \r\n */
   p = buffer;
   while ((p = strstr(p, "\n")) != NULL) {

      if (p > buffer && *(p - 1) == '\r') {
         p++;
         continue;
      }

      if ((int) strlen(buffer) + 2 >= bufsize) {
         free(tmpbuf);
         return;
      }

      strlcpy(tmpbuf, p, bufsize);
      *(p++) = '\r';
      strlcpy(p, tmpbuf, bufsize - ((int) p - (int) buffer));
      p++;
   }

   xfree(tmpbuf);
#endif
}

/*------------------------------------------------------------------*/

int save_admin_config(char *section, char *buffer, char *error)
{
   int fh, i, length;
   char *buf, *buf2, *p1, *p2;

   error[0] = 0;

   fh = open(config_file, O_RDWR | O_BINARY, 644);
   if (fh < 0) {
      sprintf(error, loc("Cannot open file <b>%s</b>"), config_file);
      strcat(error, ": ");
      strcat(error, strerror(errno));
      return 0;
   }

   /* read previous contents */
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   buf = xmalloc(length + strlen(buffer) + 10);
   read(fh, buf, length);
   buf[length] = 0;

   /* find previous logbook config */
   p1 = (char *) find_section(buf, section);
   p2 = (char *) find_next_section(p1 + 1);

   /* save tail */
   buf2 = NULL;

   if (p2)
      buf2 = xstrdup(p2);

   /* combine old and new config */
   sprintf(p1, "[%s]\r\n", section);
   strcat(p1, buffer);
   strcat(p1, "\r\n\r\n");

   if (p2) {
      strlcat(p1, buf2, length + strlen(buffer) + 1);
      xfree(buf2);
   }

   adjust_crlf(buf, length + strlen(buffer) + 10);

   lseek(fh, 0, SEEK_SET);
   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(error, loc("Cannot write to <b>%s</b>"), config_file);
      strcat(error, ": ");
      strcat(error, strerror(errno));
      close(fh);
      xfree(buf);
      return 0;
   }

   TRUNCATE(fh);

   close(fh);
   xfree(buf);

   /* force re-read of config file */
   check_config_file(TRUE);

   return 1;
}

/*------------------------------------------------------------------*/

int change_config_line(LOGBOOK * lbs, char *option, char *old_value, char *new_value)
{
   int fh, i, j, n, length, bufsize;
   char str[NAME_LENGTH], *buf, *buf2, *p1, *p2, *p3;
   char list[MAX_N_LIST][NAME_LENGTH], line[NAME_LENGTH];

   fh = open(config_file, O_RDWR | O_BINARY, 644);
   if (fh < 0) {
      sprintf(str, loc("Cannot open file <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      show_error(str);
      return 0;
   }

   /* read previous contents */
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   bufsize = 2 * (length + strlen(new_value) + 10);
   buf = xmalloc(bufsize);
   read(fh, buf, length);
   buf[length] = 0;

   /* find location of option */
   p1 = (char *) find_param(buf, lbs->name, option);
   if (p1 == NULL)
      return 0;

   p2 = strchr(p1, '=');
   if (p2 == 0)
      return 0;

   p2++;
   while (*p2 == ' ' || *p2 == '\t')
      p2++;

   strlcpy(line, p2, sizeof(line));
   if (strchr(line, '\r'))
      *strchr(line, '\r') = 0;
   if (strchr(line, '\n'))
      *strchr(line, '\n') = 0;
   n = strbreak(line, list, MAX_N_LIST, ",");

   /* save tail */
   p3 = strchr(p2, '\n');
   if (p3 && *(p3 - 1) == '\r')
      p3--;

   buf2 = NULL;
   if (p3)
      buf2 = xstrdup(p3);

   if (old_value[0]) {
      for (i = 0; i < n; i++) {
         if (strieq(old_value, list[i])) {
            if (new_value[0]) {
               /* rename value */
               strcpy(list[i], new_value);
            } else {
               /* delete value */
               for (j = i; j < n - 1; j++)
                  strcpy(list[j], list[j + 1]);
               n--;
            }
            break;
         }
      }
   } else {
      if (n < MAX_N_LIST)
         strcpy(list[n++], new_value);
   }

   /* write new option list */
   for (i = 0; i < n; i++) {
      strcpy(p2, list[i]);
      if (i < n - 1)
         strcat(p2, ", ");
      p2 += strlen(p2);
   }

   /* append tail */
   if (buf2) {
      strlcat(p2, buf2, length + strlen(new_value) + 10);
      xfree(buf2);
   }

   adjust_crlf(buf, bufsize);

   lseek(fh, 0, SEEK_SET);
   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(str, loc("Cannot write to <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      show_error(str);
      close(fh);
      xfree(buf);
      return 0;
   }

   TRUNCATE(fh);

   close(fh);
   xfree(buf);

   /* force re-read of config file */
   check_config_file(TRUE);

   return 1;
}

/*------------------------------------------------------------------*/

int delete_logbook(LOGBOOK * lbs, char *error)
{
   int fh, i, length;
   char *buf, *p1, *p2;

   error[0] = 0;

   fh = open(config_file, O_RDWR | O_BINARY, 644);
   if (fh < 0) {
      sprintf(error, loc("Cannot open file <b>%s</b>"), config_file);
      strcat(error, ": ");
      strcat(error, strerror(errno));
      return 0;
   }

   /* remove logbook name in groups */
   change_logbook_in_group(lbs, "");

   /* read previous contents */
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   buf = xmalloc(length + 1);
   read(fh, buf, length);
   buf[length] = 0;

   /* find logbook config */
   p1 = (char *) find_section(buf, lbs->name);
   p2 = (char *) find_next_section(p1 + 1);

   if (p2)
      strlcpy(p1, p2, strlen(p2) + 1);
   else
      *p1 = 0;

   lseek(fh, 0, SEEK_SET);
   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(error, loc("Cannot write to <b>%s</b>"), config_file);
      strcat(error, ": ");
      strcat(error, strerror(errno));
      close(fh);
      xfree(buf);
      return 0;
   }

   TRUNCATE(fh);

   close(fh);
   xfree(buf);

   /* force re-read of config file */
   check_config_file(TRUE);
   el_index_logbooks();

   return 1;
}

/*------------------------------------------------------------------*/

int rename_logbook(LOGBOOK * lbs, char *new_name)
{
   int fh, i, length, bufsize;
   char *buf, *buf2, *p1, *p2;
   char str[256], lb_dir[256], old_dir[256], new_dir[256];

   fh = open(config_file, O_RDWR | O_BINARY, 644);
   if (fh < 0) {
      sprintf(str, loc("Cannot open file <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      show_error(str);
      return 0;
   }

   /* rename logbook file */
   if (!getcfg(lbs->name, "Subdir", str, sizeof(str))) {
      strlcpy(lb_dir, logbook_dir, sizeof(lb_dir));
      if (lb_dir[strlen(lb_dir) - 1] != DIR_SEPARATOR)
         strlcat(lb_dir, DIR_SEPARATOR_STR, sizeof(lb_dir));

      sprintf(old_dir, "%s%s", lb_dir, lbs->name);
      sprintf(new_dir, "%s%s", lb_dir, new_name);
      rename(old_dir, new_dir);
   }

   /* change logbook name in groups */
   change_logbook_in_group(lbs, new_name);

   /* read previous contents */
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   bufsize = 2 * (length + strlen(new_name) + 10);
   buf = xmalloc(bufsize);
   read(fh, buf, length);
   buf[length] = 0;

   /* find logbook config */
   p1 = (char *) find_section(buf, lbs->name);
   p2 = strchr(p1, ']');
   if (p2 == NULL) {
      close(fh);
      xfree(buf);
      show_error(loc("Syntax error in config file"));
      return 0;
   }
   p2++;

   /* save tail */
   buf2 = xstrdup(p2);

   /* replace logbook name */
   sprintf(p1, "[%s]", new_name);

   strlcat(p1, buf2, length + strlen(new_name) + 1);
   xfree(buf2);

   adjust_crlf(buf, bufsize);

   lseek(fh, 0, SEEK_SET);
   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(str, loc("Cannot write to <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      show_error(str);
      close(fh);
      xfree(buf);
      return 0;
   }

   TRUNCATE(fh);

   close(fh);
   xfree(buf);

   /* force re-read of config file */
   check_config_file(TRUE);
   el_index_logbooks();

   return 1;
}

/*------------------------------------------------------------------*/

int create_logbook(LOGBOOK * oldlbs, char *logbook, char *templ)
{
   int fh, i, length, bufsize, templ_length;
   char *buf, *p1, *p2, str[256];

   fh = open(config_file, O_RDWR | O_BINARY, 644);
   if (fh < 0) {
      sprintf(str, loc("Cannot open file <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      show_error(str);
      return 0;
   }

   /* add logbook to current group */
   add_logbook_to_group(oldlbs, logbook);

   /* read previous contents */
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   bufsize = 2 * (2 * length + 1);
   buf = xmalloc(bufsize);
   read(fh, buf, length);
   buf[length] = 0;
   templ_length = 0;
   p2 = NULL;

   /* find template logbook */
   if (templ[0]) {
      p1 = (char *) find_section(buf, templ);
      p2 = (char *) find_next_section(p1 + 1);
   } else
      p1 = NULL;

   if (p1) {
      p1 = strchr(p1, ']');
      if (p1)
         while (*p1 == ']' || *p1 == '\r' || *p1 == '\n')
            p1++;

      if (p2)
         templ_length = (int) p2 - (int) p1;
      else
         templ_length = strlen(p1);
   }

   /* insert single blank line after last logbook */
   p2 = buf + strlen(buf) - 1;

   while (p2 > buf && (*p2 == '\r' || *p2 == '\n' || *p2 == ' ' || *p2 == '\t')) {
      *p2 = 0;
      p2--;
   }
   if (p2 > buf)
      p2++;

   strcat(p2, "\r\n\r\n[");
   strcat(p2, logbook);
   strcat(p2, "]\r\n");
   if (p1) {
      p2 = buf + strlen(buf);
      strncpy(p2, p1, templ_length);
      p2[templ_length] = 0;
   }

   adjust_crlf(buf, bufsize);

   lseek(fh, 0, SEEK_SET);
   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(str, loc("Cannot write to <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      show_error(str);
      close(fh);
      xfree(buf);
      return 0;
   }

   TRUNCATE(fh);

   close(fh);
   xfree(buf);

   /* force re-read of config file */
   check_config_file(TRUE);
   el_index_logbooks();

   return 1;
}

/*------------------------------------------------------------------*/

int save_config(char *buffer, char *error)
{
   int fh, i;
   char *buf;

   error[0] = 0;

   fh = open(config_file, O_RDWR | O_BINARY | O_CREAT, 0644);
   if (fh < 0) {
      sprintf(error, loc("Cannot open file <b>%s</b>"), config_file);
      strcat(error, ": ");
      strcat(error, strerror(errno));
      return 0;
   }

   buf = xmalloc(strlen(buffer) * 2);
   strlcpy(buf, buffer, strlen(buffer) * 2);
   adjust_crlf(buf, strlen(buffer) * 2);

   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(error, loc("Cannot write to <b>%s</b>"), config_file);
      strcat(error, ": ");
      strcat(error, strerror(errno));
      close(fh);
      return 0;
   }

   TRUNCATE(fh);

   close(fh);

   /* force re-read of config file */
   check_config_file(TRUE);

   return 1;
}

/*------------------------------------------------------------------*/

int save_user_config(LOGBOOK * lbs, char *user, BOOL new_user, BOOL activate)
{
   char file_name[256], str[256], *pl, new_pwd[80], new_pwd2[80];
   char smtp_host[256], email_addr[256], mail_from[256], subject[256], mail_text[2000];
   char admin_user[80], enc_pwd[80], url[256];
   int i, self_register;
   PMXML_NODE node, subnode;

   /* check for full name */
   if (!isparam("new_full_name") || *getparam("new_full_name") == 0) {
      sprintf(str, loc("Please enter \"%s\""), loc("Full name"));
      show_error(str);
      return 0;
   }

   /* check self register flag */
   self_register = 0;
   if (getcfg(lbs->name, "Self register", str, sizeof(str)))
      self_register = atoi(str);

   if (!activate) {
      /* check for hidden password */
      if (isparam("hpwd")) {
         strcpy(new_pwd, getparam("hpwd"));
      } else {
         /* check if passwords match */
         do_crypt(getparam("newpwd"), new_pwd);
         do_crypt(getparam("newpwd2"), new_pwd2);

         if (strcmp(new_pwd, new_pwd2) != 0) {
            show_error(loc("New passwords do not match, please retype"));
            return 0;
         }
      }

      /* check if user exists */
      if (new_user && self_register == 3) {
         if (get_user_line(lbs, user, NULL, NULL, NULL, NULL, NULL) == 1) {
            sprintf(str, "%s \"%s\" %s", loc("Login name"), user, loc("exists already"));
            show_error(str);
            return 0;
         }
      }
   }

   /* if register through selection page, use first logbook with same password file */
   if (lbs == NULL) {
      getcfg(NULL, "password file", file_name, sizeof(file_name));
      for (i = 0; lb_list[i].name[0]; i++) {
         getcfg(lb_list[i].name, "password file", str, sizeof(str));
         if (strieq(file_name, str))
            break;
      }
      if (lb_list[i].name[0] == 0)
         lbs = &lb_list[0];
      else
         lbs = &lb_list[i];
   }

   if (activate || !new_user || self_register != 3) {   /* do not save in mode 3 */
      getcfg(lbs->name, "Password file", str, sizeof(str));

      if (lbs->pwd_xml_tree == NULL)
         return 0;

      sprintf(str, "/list/user[name=%s]", user);
      node = mxml_find_node(lbs->pwd_xml_tree, str);

      if (node && new_user) {
         sprintf(str, "%s \"%s\" %s", loc("Login name"), user, loc("exists already"));
         show_error(str);
         return 0;
      }

      if (new_user) {
         node = mxml_find_node(lbs->pwd_xml_tree, "/list");
         node = mxml_add_node(node, "user", NULL);

         mxml_add_node(node, "name", getparam("new_user_name"));
         if (activate)
            mxml_add_node(node, "password", getparam("encpwd"));
         else
            mxml_add_node(node, "password", new_pwd);

         mxml_add_node(node, "full_name", getparam("new_full_name"));
         mxml_add_node(node, "last_logout", "0");
         mxml_add_node(node, "last_activity", "0");
         mxml_add_node(node, "email", getparam("new_user_email"));

      } else {
         /* replace record */
         mxml_replace_subvalue(node, "name", getparam("new_user_name"));
         mxml_replace_subvalue(node, "password", new_pwd);
         mxml_replace_subvalue(node, "full_name", getparam("new_full_name"));
         mxml_replace_subvalue(node, "email", getparam("new_user_email"));
      }

      subnode = mxml_find_node(node, "email_notify");
      if (subnode)
         mxml_delete_node(subnode);
      mxml_add_node(node, "email_notify", NULL);
      subnode = mxml_find_node(node, "email_notify");
      for (i = 0; lb_list[i].name[0]; i++) {
         sprintf(str, "sub_lb%d", i);
         if (getparam(str) && atoi(getparam(str)))
            mxml_add_node(subnode, "logbook", lb_list[i].name);
      }

      if (get_password_file(lbs, file_name, sizeof(file_name)))
         mxml_write_tree(file_name, lbs->pwd_xml_tree);
   }

   /* if requested, send notification email to admin user */
   if (new_user && (self_register == 2 || self_register == 3)
       && !isparam("admin")) {
      if (!getcfg("global", "SMTP host", smtp_host, sizeof(smtp_host))) {
         show_error(loc("No SMTP host defined in [global] section of configuration file"));
         return 0;
      }

      /* try to get URL from referer */
      if (!getcfg("global", "URL", url, sizeof(url))) {
         if (referer[0])
            strcpy(url, referer);
         else {
            if (elog_tcp_port == 80)
               sprintf(url, "http://%s/", host_name);
            else
               sprintf(url, "http://%s:%d/", host_name, elog_tcp_port);
         }
      } else {
         if (url[strlen(url) - 1] != '/')
            strlcat(url, "/", sizeof(url));
         if (lbs) {
            strlcat(url, lbs->name_enc, sizeof(url));
            strlcat(url, "/", sizeof(url));
         }
      }

      retrieve_email_from(lbs, mail_from, NULL);

      if (activate) {
         sprintf(subject, loc("Your ELOG account has been activated"));
         sprintf(mail_text, loc("Your ELOG account has been activated on host"));
         sprintf(mail_text + strlen(mail_text), " %s", host_name);
         sprintf(mail_text + strlen(mail_text), ".\r\n\r\n");
         sprintf(url + strlen(url), "?cmd=Login&unm=%s", getparam("new_user_name"));
         sprintf(mail_text + strlen(mail_text), "%s %s\r\n", loc("You can access it at"), url);

         sendmail(lbs, smtp_host, mail_from, getparam("new_user_email"), subject, mail_text,
                  TRUE, url, "text/plain", NULL, NULL, 0);
      } else {
         if (getcfg(lbs->name, "Admin user", admin_user, sizeof(admin_user))) {
            pl = strtok(admin_user, " ,");
            while (pl) {
               get_user_line(lbs, pl, NULL, NULL, email_addr, NULL, NULL);
               if (email_addr[0]) {
                  /* compose subject */
                  if (self_register == 3) {
                     if (lbs)
                        sprintf(subject, loc("Registration request on logbook \"%s\""), lbs->name);
                     else
                        sprintf(subject, loc("Registration request on host \"%s\""), host_name);
                     sprintf(mail_text, loc("A new ELOG user wants to register on \"%s\""), host_name);
                  } else {
                     if (lbs)
                        sprintf(subject, loc("User \"%s\" registered on logbook \"%s\""),
                                getparam("new_user_name"), lbs->name);
                     else
                        sprintf(subject, loc("User \"%s\" registered on host \"%s\""),
                                getparam("new_user_name"), host_name);

                     sprintf(mail_text, loc("A new ELOG user has been registered on %s"), host_name);
                  }

                  sprintf(mail_text + strlen(mail_text), "\r\n\r\n");

                  if (lbs)
                     sprintf(mail_text + strlen(mail_text), "%s             : %s\r\n",
                             loc("Logbook"), lbs->name);
                  else
                     sprintf(mail_text + strlen(mail_text), "%s                : %s\r\n",
                             loc("Host"), host_name);

                  sprintf(mail_text + strlen(mail_text), "%s          : %s\r\n",
                          loc("Login name"), getparam("new_user_name"));
                  sprintf(mail_text + strlen(mail_text), "%s           : %s\r\n",
                          loc("Full name"), getparam("new_full_name"));
                  sprintf(mail_text + strlen(mail_text), "%s               : %s\r\n",
                          loc("Email"), getparam("new_user_email"));

                  if (self_register == 3) {
                     sprintf(mail_text + strlen(mail_text), "\r\n%s:\r\n",
                             loc("Hit following URL to activate that account"));

                     sprintf(mail_text + strlen(mail_text), "\r\nURL                 : %s", url);

                     strcpy(str, getparam("new_full_name"));
                     url_encode(str, sizeof(str));
                     do_crypt(getparam("newpwd"), enc_pwd);
                     url_encode(enc_pwd, sizeof(enc_pwd));
                     sprintf(mail_text + strlen(mail_text), "?cmd=Activate&new_user_name=%s&new_full_name=%s",
                             getparam("new_user_name"), str);
                     sprintf(mail_text + strlen(mail_text), "&new_user_email=%s", getparam("new_user_email"));

                     for (i = 0; lb_list[i].name[0]; i++) {
                        sprintf(str, "sub_lb%d", i);
                        if (isparam(str) && atoi(getparam(str)) == 1)
                           sprintf(mail_text + strlen(mail_text), "&amp;%s=1", str);
                     }

                     sprintf(mail_text + strlen(mail_text), "&encpwd=%s&unm=%s\r\n", enc_pwd, pl);
                  } else {
                     sprintf(mail_text + strlen(mail_text),
                             "\r\n%s URL         : %s?cmd=Config&cfg_user=%s&unm=%s\r\n",
                             loc("Logbook"), url, getparam("new_user_name"), pl);
                  }

                  sendmail(lbs, smtp_host, mail_from, email_addr, subject, mail_text, TRUE,
                           url, "text/plain", NULL, NULL, 0);
               }

               pl = strtok(NULL, " ,");
            }
         } else {
            show_error(loc("No admin user has been defined in configuration file"));
            return 0;
         }

         if (self_register == 3) {
            sprintf(str, "?cmd=%s", loc("Requested"));
            redirect(lbs, str);
            return 0;
         }
      }
   }

   /* if user name changed, set cookie */
   if (strcmp(user, getparam("new_user_name")) != 0 && strcmp(user, getparam("unm")) == 0) {
      set_login_cookies(lbs, getparam("new_user_name"), new_pwd);
      return 0;
   }

   /* if new user, login as this user */
   if (new_user && !*getparam("unm")) {
      set_login_cookies(lbs, getparam("new_user_name"), new_pwd);
      return 0;
   }

   return 1;
}

/*------------------------------------------------------------------*/

int remove_user(LOGBOOK * lbs, char *user)
{
   char file_name[256], str[1000];
   PMXML_NODE node;

   if (lbs->pwd_xml_tree == NULL) {
      show_error("No password file loaded");
      return FALSE;
   }

   sprintf(str, "/list/user[name=%s]", user);
   node = mxml_find_node(lbs->pwd_xml_tree, str);
   if (node == NULL) {
      sprintf("User \"%s\" not found in password fiel", user);
      show_error(str);
      return FALSE;
   }

   mxml_delete_node(node);

   if (get_password_file(lbs, file_name, sizeof(file_name))) {
      if (!mxml_write_tree(file_name, lbs->pwd_xml_tree)) {
         sprintf(str, loc("Cannot write to file <b>%s</b>"), file_name);
         strcat(str, ": ");
         strcat(str, strerror(errno));
         show_error(str);
         return FALSE;
      }
   }

   return FALSE;
}

/*------------------------------------------------------------------*/

int ascii_compare(const void *s1, const void *s2)
{
   return stricmp(*(char **) s1, *(char **) s2);
}

/*------------------------------------------------------------------*/

void show_config_page(LOGBOOK * lbs)
{
   char str[256], user[80], password[80], full_name[80], user_email[80], logbook[256];
   char **user_list;
   int i, n;
   BOOL email_notify[1000];

   if (lbs)
      strcpy(logbook, lbs->name);
   else
      strcpy(logbook, "global");

   /* get user */
   strcpy(user, getparam("unm"));
   if (isparam("cfg_user"))
      strcpy(user, getparam("cfg_user"));

   /*---- header ----*/

   show_standard_header(lbs, TRUE, loc("ELOG user config"), ".", FALSE, NULL);

   /*---- javascript to warn removal of user ----*/
   rsprintf("<script type=\"text/javascript\">\n");
   rsprintf("<!--\n\n");
   rsprintf("function chkrem()\n");
   rsprintf("{\n");
   sprintf(str, loc("Really remove user %s?"), user);
   rsprintf("    var subm = confirm(\"%s\");\n", str);
   rsprintf("    return subm;\n");
   rsprintf("}\n\n");
   rsprintf("//-->\n");
   rsprintf("</script>\n\n");

   /*---- title ----*/

   show_standard_title(logbook, "", 0);

   /*---- menu buttons ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");

   rsprintf("<input type=hidden name=cmd value=\"%s\">\n", loc("Config"));      // for select javascript
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Save"));
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Back"));
   rsprintf("<input type=hidden name=config value=\"%s\">\n", user);

   rsprintf("</span></td></tr>\n\n");

   /* table for two-column items */
   rsprintf("<tr><td class=\"form2\">");
   rsprintf("<table width=\"100%%\" cellspacing=0>\n");

   /*---- if admin user, show user list ----*/

   if (is_admin_user(logbook, getparam("unm"))) {
      rsprintf("<input type=hidden name=admin value=1>\n");
      rsprintf("<tr><td nowrap width=\"10%%\">%s:</td>\n", loc("Select user"));
      rsprintf("<td><select name=cfg_user onChange=\"document.form1.submit()\">\n");

      /* count user list */
      for (n = 0;; n++) {
         if (!enum_user_line(lbs, n, str, sizeof(str)))
            break;
      }

      /* allocate list of users and populate it */
      user_list = xcalloc(sizeof(char *), n);
      for (i = 0; i < n; i++)
         user_list[i] = xcalloc(NAME_LENGTH, 1);

      for (i = 0; i < n; i++)
         enum_user_line(lbs, i, user_list[i], NAME_LENGTH);

      /* sort list */
      qsort(user_list, n, sizeof(char *), ascii_compare);

      for (i = 0; i < n; i++) {
         if (strcmp(user_list[i], user) == 0)
            rsprintf("<option selected value=\"%s\">%s\n", user_list[i], user_list[i]);
         else
            rsprintf("<option value=\"%s\">%s\n", user_list[i], user_list[i]);
      }

      for (i = 0; i < n; i++)
         xfree(user_list[i]);
      xfree(user_list);

      rsprintf("</select>\n");

      /* show "update" button only of javascript is not enabled */
      rsprintf("<noscript>\n");
      rsprintf("<input type=submit value=\"%s\">\n", loc("Update"));
      rsprintf("</noscript>\n");
   }

   /*---- entry form ----*/

   rsprintf("<tr><td nowrap width=\"15%%\">%s:</td>\n", loc("Login name"));

   if (get_user_line(lbs, user, password, full_name, user_email, email_notify, NULL) != 1)
      sprintf(str, loc("User [%s] has been deleted"), user);
   else
      strcpy(str, user);

   rsprintf("<td><input type=text size=40 name=new_user_name value=\"%s\"></td></tr>\n", str);

   rsprintf("<tr><td nowrap width=\"15%%\">%s:</td>\n", loc("Full name"));
   rsprintf("<td><input type=text size=40 name=new_full_name value=\"%s\"></tr>\n", full_name);

   rsprintf("<tr><td nowrap width=\"15%%\">Email:</td>\n");
   rsprintf("<td><input type=text size=40 name=new_user_email value=\"%s\"></td></tr>\n", user_email);

   rsprintf("<tr><td width=\"15%%\">%s:\n", loc("Subscribe to logbooks"));

   rsprintf("<br><span class=\"selcomment\"><b>(%s)</b></span>\n",
            loc("enable automatic email notifications"));

   rsprintf("<td>\n");

   for (i = 0; lb_list[i].name[0]; i++) {

      if (!getcfg_topgroup() || strieq(getcfg_topgroup(), lb_list[i].top_group)) {

         /* check if user has access */
         if (!isparam("unm") || check_login_user(&lb_list[i], getparam("unm"))) {

            if (email_notify[i])
               rsprintf("<input type=checkbox checked id=\"lb%d\" name=\"sub_lb%d\" value=\"1\">\n", i, i);
            else
               rsprintf("<input type=checkbox id=\"lb%d\" name=\"sub_lb%d\" value=\"1\">\n", i, i);
            rsprintf("<label for=\"lb%d\">%s</label><br>\n", i, lb_list[i].name);
         }
      }
   }

   if (i > 2) {
      rsprintf("<script language=\"JavaScript\" type=\"text/javascript\">\n");
      rsprintf("<!--\n");
      rsprintf("function SetNone()\n");
      rsprintf("  {\n");
      rsprintf("  for (var i = 0; i < document.form1.elements.length; i++)\n");
      rsprintf("    {\n");
      rsprintf("    if( document.form1.elements[i].type == 'checkbox' )\n");
      rsprintf("      document.form1.elements[i].checked = false;\n");
      rsprintf("    }\n");
      rsprintf("  }\n");
      rsprintf("function SetAll()\n");
      rsprintf("  {\n");
      rsprintf("  for (var i = 0; i < document.form1.elements.length; i++)\n");
      rsprintf("    {\n");
      rsprintf("    if( document.form1.elements[i].type == 'checkbox' )\n");
      rsprintf("      document.form1.elements[i].checked = true;\n");
      rsprintf("    }\n");
      rsprintf("  }\n");
      rsprintf("//-->\n");
      rsprintf("</script>\n");

      rsprintf("<input type=button value=\"%s\" onClick=\"SetAll();\">\n", loc("Set all"));
      rsprintf("<input type=button value=\"%s\" onClick=\"SetNone();\">\n", loc("Set none"));
   }

   rsprintf("</td></tr>\n");

   rsprintf("</table></td></tr>\n");

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Change password"));
   rsprintf("<input type=submit name=cmd value=\"%s\" onClick=\"return chkrem();\">\n", loc("Remove user"));

   if (is_admin_user(logbook, getparam("unm"))) {
      rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("New user"));
      sprintf(str, loc("Change config file"));
      rsprintf("<input type=submit name=cmd value=\"%s\">\n", str);
   }

   /* hidden field for password */
   rsprintf("<input type=hidden name=hpwd value=\"%s\">\n", password);

   rsprintf("</span></td></tr></table>\n\n");
   show_bottom_text(lbs);
   rsprintf("</form></body></html>\r\n");
}

/*------------------------------------------------------------------*/

void show_forgot_pwd_page(LOGBOOK * lbs)
{
   int i;
   char str[1000], login_name[256], full_name[256], user_email[256],
       name[256], pwd[256], redir[256], pwd_encrypted[256], smtp_host[256],
       mail_from[256], subject[256], mail_text[1000], url[1000];

   if (isparam("login_name")) {
      /* seach in pwd file */

      strcpy(name, getparam("login_name"));

      for (i = 0;; i++) {
         if (!enum_user_line(lbs, i, login_name, sizeof(login_name)))
            break;

         get_user_line(lbs, login_name, NULL, full_name, user_email, NULL, NULL);

         if (strieq(name, login_name)
             || strieq(name, full_name)
             || strieq(name, user_email)) {
            if (user_email[0] == 0) {
               sprintf(str, loc("No Email address registered with user name <i>\"%s\"</i>"), name);
               show_error(str);
               return;
            }

            /* create random password */
            srand((unsigned int) time(NULL));
            for (i = 0; i < 8; i++)
               str[i] = 'A' + (rand() % 25);
            str[i] = 0;
            base64_encode((unsigned char *) str, (unsigned char *) pwd);
            do_crypt(pwd, pwd_encrypted);

            /* send email with new password */
            if (!getcfg("global", "SMTP host", smtp_host, sizeof(smtp_host))) {
               show_error(loc("No SMTP host defined in [global] section of configuration file"));
               return;
            }

            /* try to get URL from referer */
            if (!getcfg("global", "URL", url, sizeof(url))) {
               if (referer[0])
                  strcpy(url, referer);
               else {
                  if (elog_tcp_port == 80)
                     sprintf(url, "http://%s/", host_name);
                  else
                     sprintf(url, "http://%s:%d/", host_name, elog_tcp_port);
               }
            } else {
               if (url[strlen(url) - 1] != '/')
                  strlcat(url, "/", sizeof(url));
               if (lbs) {
                  strlcat(url, lbs->name_enc, sizeof(url));
                  strlcat(url, "/", sizeof(url));
               }
            }

            url_slash_encode(pwd, sizeof(pwd));
            sprintf(redir, "?cmd=%s&oldpwd=%s", loc("Change password"), pwd);
            url_encode(redir, sizeof(redir));
            sprintf(str, "?redir=%s&uname=%s&upassword=%s", redir, login_name, pwd);
            strlcat(url, str, sizeof(url));

            retrieve_email_from(lbs, mail_from, NULL);

            if (lbs)
               sprintf(subject, loc("Password recovery for ELOG %s"), lbs->name);
            else
               sprintf(subject, loc("Password recovery for ELOG %s"), host_name);

            sprintf(mail_text, loc("A new password has been created for you on host %s"), host_name);
            strlcat(mail_text, ".\r\n", sizeof(mail_text));
            strlcat(mail_text,
                    loc
                    ("Please log on by clicking on following link and change your password"),
                    sizeof(mail_text));
            strlcat(mail_text, ":\r\n\r\n", sizeof(mail_text));
            strlcat(mail_text, url, sizeof(mail_text));
            strlcat(mail_text, "\r\n\r\n", sizeof(mail_text));
            sprintf(mail_text + strlen(mail_text), "ELOG Version %s\r\n", VERSION);

            if (sendmail(lbs, smtp_host, mail_from, user_email, subject, mail_text, TRUE,
                         url, "text/plain", NULL, NULL, 0)
                != -1) {
               /* save new password */
               change_pwd(lbs, login_name, pwd_encrypted);

               /* show notification web page */
               show_standard_header(lbs, FALSE, loc("ELOG password recovery"), "", FALSE, NULL);

               rsprintf("<table class=\"dlgframe\" cellspacing=0 align=center>");
               rsprintf("<tr><td class=\"dlgtitle\">\n");
               rsprintf(loc("Email notification"));
               rsprintf("</td></tr>\n");

               rsprintf("<tr><td align=center class=\"dlgform\">\n");
               rsprintf(loc("A new password for user <i>\"%s\"</i> has been sent to %s"),
                        full_name, user_email);
               rsprintf("</td></tr></table>\n");
               show_bottom_text(lbs);
               rsprintf("</body></html>\n");
               return;
            } else {
               sprintf(str, loc("Error sending Email via <i>\"%s\"</i>"), smtp_host);
               show_error(str);
               return;
            }
         }
      }

      if (strchr(name, '@'))
         sprintf(str, loc("Email address <i>\"%s\"</i> not registered"), name);
      else
         sprintf(str, loc("User name <i>\"%s\"</i> not registered"), name);

      show_error(str);

      return;
   } else {
      /*---- header ----*/

      show_standard_header(lbs, TRUE, loc("ELOG password recovery"), NULL, FALSE, NULL);

      rsprintf("<table class=\"dlgframe\" cellspacing=0 align=center>");

      /*---- entry form ----*/

      rsprintf("<tr><td class=\"dlgtitle\">%s</td></tr>\n", loc("Enter your user name or email address"));


      rsprintf("<tr><td align=center class=\"dlgform\">\n");
      rsprintf("<input type=hidden name=cmd value=%s>\n", loc("Forgot"));
      rsprintf("<input type=text size=40 name=login_name></td></tr>\n");

      rsprintf("<tr><td align=center class=\"dlgform\"><input type=submit value=\"%s\">\n", loc("Submit"));

      rsprintf("</td></tr></table>\n\n");
      show_bottom_text(lbs);
      rsprintf("</form></body></html>\r\n");
   }
}


/*------------------------------------------------------------------*/

void show_new_user_page(LOGBOOK * lbs)
{
   int i;

   /*---- header ----*/

   show_html_header(lbs, TRUE, loc("ELOG new user"), TRUE, FALSE, NULL);
   rsprintf("<body><center><br><br>\n");
   show_top_text(lbs);
   rsprintf("<form name=form1 method=\"GET\" action=\".\">\n\n");

   /*---- title ----*/

   if (lbs)
      show_standard_title(lbs->name, "", 1);
   else
      show_standard_title("ELOG", "", 1);

   /*---- menu buttons ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");

   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Save"));
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Cancel"));
   rsprintf("</span></td></tr>\n\n");

   /* table for two-column items */
   rsprintf("<tr><td class=\"form2\">");
   rsprintf("<table width=\"100%%\" cellspacing=0>\n");

   /*---- entry form ----*/

   rsprintf("<tr><td nowrap>%s:</td>\n", loc("Login name"));
   rsprintf("<td><input type=text size=40 name=new_user_name></td>\n");
   rsprintf("<td nowrap align=left><font size=2><i>(%s)</i></font></td></tr>\n",
            loc("name may not contain blanks"));

   rsprintf("<tr><td nowrap>%s:</td>\n", loc("Full name"));
   rsprintf("<td colspan=2><input type=text size=40 name=new_full_name></tr>\n");

   rsprintf("<tr><td nowrap>Email:</td>\n");
   rsprintf("<td colspan=2><input type=text size=40 name=new_user_email></tr>\n");

   //rsprintf("<tr><td colspan=3>%s:&nbsp;\n", loc("Enable email notifications"));
   //rsprintf("<input type=checkbox checked name=email_notify value=all></tr>\n");


   rsprintf("<tr><td nowrap>%s:\n", loc("Subscribe to logbooks"));

   rsprintf("<br><span class=\"selcomment\"><b>(%s)</b></span>\n",
            loc("enable automatic email notifications"));

   rsprintf("<td>\n");

   for (i = 0; lb_list[i].name[0]; i++) {

      if (!getcfg_topgroup() || strieq(getcfg_topgroup(), lb_list[i].top_group)) {

         rsprintf("<input type=checkbox checked id=\"lb%d\" name=\"sub_lb%d\" value=\"1\">\n", i, i);
         rsprintf("<label for=\"lb%d\">%s</label><br>\n", i, lb_list[i].name);
      }
   }

   if (i > 2) {
      rsprintf("<script language=\"JavaScript\" type=\"text/javascript\">\n");
      rsprintf("<!--\n");
      rsprintf("function SetNone()\n");
      rsprintf("  {\n");
      rsprintf("  for (var i = 0; i < document.form1.elements.length; i++)\n");
      rsprintf("    {\n");
      rsprintf("    if( document.form1.elements[i].type == 'checkbox' )\n");
      rsprintf("      document.form1.elements[i].checked = false;\n");
      rsprintf("    }\n");
      rsprintf("  }\n");
      rsprintf("function SetAll()\n");
      rsprintf("  {\n");
      rsprintf("  for (var i = 0; i < document.form1.elements.length; i++)\n");
      rsprintf("    {\n");
      rsprintf("    if( document.form1.elements[i].type == 'checkbox' )\n");
      rsprintf("      document.form1.elements[i].checked = true;\n");
      rsprintf("    }\n");
      rsprintf("  }\n");
      rsprintf("//-->\n");
      rsprintf("</script>\n");

      rsprintf("<input type=button value=\"%s\" onClick=\"SetAll();\">\n", loc("Set all"));
      rsprintf("<input type=button value=\"%s\" onClick=\"SetNone();\">\n", loc("Set none"));
   }

   rsprintf("</td></tr>\n");

   rsprintf("<tr><td nowrap>%s:</td>\n", loc("Password"));
   rsprintf("<td colspan=2><input type=password size=40 name=newpwd>\n");

   rsprintf("<tr><td nowrap>%s:</td>\n", loc("Retype password"));
   rsprintf("<td colspan=2><input type=password size=40 name=newpwd2>\n");

   rsprintf("</td></tr></table>\n");

   rsprintf("</td></tr></table>\n\n");
   show_bottom_text(lbs);
   rsprintf("</form></center></body></html>\r\n");
}

/*------------------------------------------------------------------*/

void show_elog_delete(LOGBOOK * lbs, int message_id)
{
   int i, status, reply, next;
   char str[256], in_reply_to[80], reply_to[MAX_REPLY_TO * 10], owner[256];
   char attrib[MAX_N_ATTR][NAME_LENGTH];

   /* redirect if confirm = NO */
   if (getparam("confirm") && *getparam("confirm")
       && strcmp(getparam("confirm"), loc("No")) == 0) {
      if (message_id) {
         sprintf(str, "%d", message_id);
         redirect(lbs, str);
      } else {
         strlcpy(str, getparam("lastcmd"), sizeof(str));
         url_decode(str);
         redirect(lbs, str);
      }
      return;
   }

   if (getparam("confirm") && *getparam("confirm")) {
      if (strcmp(getparam("confirm"), loc("Yes")) == 0) {
         if (message_id) {
            /* delete message */
            status = el_delete_message(lbs, message_id, TRUE, NULL, TRUE, TRUE);
            if (status != EL_SUCCESS) {
               sprintf(str, "%s = %d", loc("Error deleting message: status"), status);
               show_error(str);
               return;
            } else {
               strcpy(str, getparam("nextmsg"));
               if (atoi(str) == 0)
                  sprintf(str, "%d", el_search_message(lbs, EL_LAST, 0, TRUE));
               if (atoi(str) == 0)
                  redirect(lbs, "");
               else
                  redirect(lbs, str);
               return;
            }
         } else {
            for (i = reply = 0; i < atoi(getparam("nsel")); i++) {
               sprintf(str, "s%d", i);
               if (isparam(str))
                  status = el_delete_message(lbs, atoi(getparam(str)), TRUE, NULL, TRUE, TRUE);
            }

            redirect(lbs, getparam("lastcmd"));
            return;
         }
      }
   } else {
      /* check if at least one message is selected */
      if (!message_id) {
         for (i = 0; i < atoi(getparam("nsel")); i++) {
            sprintf(str, "s%d", i);
            if (isparam(str))
               break;
         }
         if (i == atoi(getparam("nsel"))) {
            show_error(loc("No entry selected for deletion"));
            return;
         }
      }

      /* check for author */
      if (getcfg(lbs->name, "Restrict edit", str, sizeof(str)) && atoi(str) == 1) {
         /* get message for reply/edit */

         el_retrieve(lbs, message_id, NULL, attr_list, attrib, lbs->n_attr, NULL, NULL,
                     NULL, NULL, NULL, NULL, NULL);

         if (!is_author(lbs, attrib, owner)) {
            sprintf(str, loc("Only user <i>%s</i> can delete this entry"), owner);
            show_error(str);
            return;
         }
      }

      /* header */
      if (message_id)
         sprintf(str, "%d", message_id);
      else
         str[0] = 0;
      show_standard_header(lbs, TRUE, "Delete ELog entry", str, FALSE, NULL);

      rsprintf("<table class=\"dlgframe\" cellspacing=0 align=center>");
      rsprintf("<tr><td class=\"dlgtitle\">\n");

      /* define hidden field for command */
      rsprintf("<input type=hidden name=cmd value=\"%s\">\n", loc("Delete"));

      if (!message_id) {
         rsprintf("%s</td></tr>\n", loc("Are you sure to delete these messages?"));

         rsprintf("<tr><td align=center class=\"dlgform\">\n");
         rsprintf("<input type=hidden name=nsel value=%s>\n", getparam("nsel"));

         if (isparam("lastcmd")) {
            strcpy(str, getparam("lastcmd"));
            rsprintf("<input type=hidden name=lastcmd value=\"%s\">\n", str);
         }

         for (i = reply = 0; i < atoi(getparam("nsel")); i++) {
            sprintf(str, "s%d", i);
            if (isparam(str)) {
               rsprintf("#%s ", getparam(str));
               rsprintf("<input type=hidden name=%s value=%s>\n", str, getparam(str));
            }

            if (!reply) {
               el_retrieve(lbs, atoi(getparam(str)), NULL, attr_list, NULL, 0, NULL, NULL,
                           in_reply_to, reply_to, NULL, NULL, NULL);
               if (reply_to[0])
                  reply = TRUE;
            }
         }

         rsprintf("</td></tr>\n");

         if (reply)
            rsprintf("<tr><td align=center class=\"dlgform\">%s</td></tr>\n", loc("and all their replies"));

      } else {
         rsprintf("%s</td></tr>\n", loc("Are you sure to delete this entry?"));

         /* check for replies */

         /* retrieve original message */
         el_retrieve(lbs, message_id, NULL, attr_list, NULL, 0, NULL, NULL, in_reply_to,
                     reply_to, NULL, NULL, NULL);

         if (reply_to[0])
            rsprintf("<tr><td align=center class=\"dlgform\">#%d<br>%s</td></tr>\n",
                     message_id, loc("and all its replies"));
         else
            rsprintf("<tr><td align=center class=\"dlgform\">#%d</td></tr>\n", message_id);

         /* put link to next message */
         next = el_search_message(lbs, EL_NEXT, message_id, TRUE);

         rsprintf("<input type=hidden name=nextmsg value=%d>\n", next);
      }

      rsprintf
          ("<tr><td align=center class=\"dlgform\"><input type=submit name=confirm value=\"%s\">\n",
           loc("Yes"));
      rsprintf("<input type=submit name=confirm value=\"%s\">\n", loc("No"));
      rsprintf("</td></tr>\n\n");
   }

   rsprintf("</table>\n");
   show_bottom_text(lbs);
   rsprintf("</body></html>\r\n");
}

/*------------------------------------------------------------------*/

void show_logbook_delete(LOGBOOK * lbs)
{
   char str[256];

   /* redirect if confirm = NO */
   if (getparam("confirm") && *getparam("confirm")
       && strcmp(getparam("confirm"), loc("No")) == 0) {

      redirect(lbs, "?cmd=Config");
      return;
   }

   if (getparam("confirm") && *getparam("confirm")) {
      if (strcmp(getparam("confirm"), loc("Yes")) == 0) {

         /* delete logbook */
         str[0] = 0;
         delete_logbook(lbs, str);
         if (str[0])
            show_error(str);
         else
            redirect(NULL, "../");
         return;
      }

   } else {


      strcpy(str, "Delete logbook");
      show_standard_header(lbs, TRUE, str, "", FALSE, NULL);

      rsprintf("<table class=\"dlgframe\" cellspacing=0 align=center>");
      rsprintf("<tr><td class=\"dlgtitle\">\n");

      /* define hidden field for command */
      rsprintf("<input type=hidden name=cmd value=\"%s\">\n", loc("Delete this logbook"));

      sprintf(str, loc("Are you sure to delete logbook \"%s\"?"), lbs->name);
      rsprintf("%s</td></tr>\n", str);

      rsprintf("<tr><td align=center class=\"dlgform\">");
      rsprintf("<input type=submit name=confirm value=\"%s\">\n", loc("Yes"));
      rsprintf("<input type=submit name=confirm value=\"%s\">\n", loc("No"));
      rsprintf("</td></tr>\n\n");
   }

   rsprintf("</table>\n");
   show_bottom_text(lbs);
   rsprintf("</body></html>\r\n");
}

/*------------------------------------------------------------------*/

void show_logbook_rename(LOGBOOK * lbs)
{
   int i;
   char str[256], lbn[256];

   if (getparam("lbname") && *getparam("lbname")) {

      /* check if logbook name exists already */
      strcpy(lbn, getparam("lbname"));
      for (i = 0; lb_list[i].name[0]; i++)
         if (strieq(lbn, lb_list[i].name)) {
            sprintf(str, loc("Logbook \"%s\" exists already, please choose different name"), lbn);
            show_error(str);
            return;
         }

      if (!rename_logbook(lbs, getparam("lbname")))
         return;

      sprintf(str, "../%s/?cmd=Config", getparam("lbname"));
      redirect(NULL, str);
      return;

   } else {


      strcpy(str, loc("Rename logbook"));
      show_standard_header(lbs, TRUE, str, "", FALSE, NULL);

      rsprintf("<table class=\"dlgframe\" cellspacing=0 align=center>");
      rsprintf("<tr><td class=\"dlgtitle\">\n");

      /* define hidden field for command */
      rsprintf("<input type=hidden name=cmd value=\"%s\">\n", loc("Rename this logbook"));

      rsprintf("%s</td></tr>\n", loc("Enter new logbook name"));

      rsprintf("<tr><td align=center class=\"dlgform\">");
      rsprintf("<input type=text name=\"lbname\"><br><br>\n");
      rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Rename this logbook"));
      rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Cancel"));
      rsprintf("</td></tr>\n\n");
   }

   rsprintf("</table>\n");
   show_bottom_text(lbs);
   rsprintf("</body></html>\r\n");
}

/*------------------------------------------------------------------*/

void show_logbook_new(LOGBOOK * lbs)
{
   char str[256], lbn[256];
   int i;

   if (getparam("lbname") && *getparam("lbname")) {

      /* check if logbook name exists already */
      strcpy(lbn, getparam("lbname"));
      for (i = 0; lb_list[i].name[0]; i++)
         if (strieq(lbn, lb_list[i].name)) {
            sprintf(str, loc("Logbook \"%s\" exists already, please choose different name"), lbn);
            show_error(str);
            return;
         }

      /* create new logbook */
      if (!create_logbook(lbs, getparam("lbname"), getparam("template")))
         return;

      strcpy(lbn, getparam("lbname"));
      url_encode(lbn, sizeof(lbn));
      sprintf(str, "../%s/?cmd=Config", lbn);
      redirect(NULL, str);
      return;
   }

   show_standard_header(lbs, TRUE, "Delete Logbook", "", FALSE, NULL);

   rsprintf("<table class=\"dlgframe\" cellspacing=0 align=center>");
   rsprintf("<tr><td class=\"dlgtitle\">\n");

   /* define hidden field for command */
   rsprintf("<input type=hidden name=cmd value=\"%s\">\n", loc("Create new logbook"));
   rsprintf("%s</td></tr>\n", loc("Create new logbook"));

   rsprintf("<tr><td align=center class=\"dlgform\">\n");
   rsprintf("%s :&nbsp;&nbsp;", loc("Logbook name"));
   rsprintf("<input type=text name=lbname>\n");
   rsprintf("</td></tr>\n");

   rsprintf("<tr><td align=center class=\"dlgform\">\n");
   rsprintf("%s : \n", loc("Use existing logbook as template"));
   rsprintf("<select name=template>\n");
   rsprintf("<option value=\"\">- %s -\n", loc("none"));
   for (i = 0; lb_list[i].name[0]; i++)
      rsprintf("<option value=\"%s\">%s\n", lb_list[i].name, lb_list[i].name);
   rsprintf("</select>\n");
   rsprintf("</td></tr>\n\n");

   rsprintf("<tr><td align=center class=\"dlgform\">\n");
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Create new logbook"));
   rsprintf("<input type=submit name=tmp value=\"%s\">\n", loc("Cancel"));
   rsprintf("</td></tr>\n\n");

   rsprintf("</table>\n");
   show_bottom_text(lbs);
   rsprintf("</body></html>\r\n");
}

/*------------------------------------------------------------------*/

int show_download_page(LOGBOOK * lbs, char *path)
{
   char file_name[256], error_str[256];
   int index, message_id, fh, i, size, delta;
   char message[TEXT_SIZE + 1000], *p, *buffer;

   if (stricmp(path, "gbl") == 0) {

      /* return complete config file */
      load_config_section(NULL, &buffer, error_str);
      if (error_str[0]) {
         rsprintf("Error loading configuration file: %s", error_str);
         return EL_FILE_ERROR;
      }

      size = strlen(buffer);
      strlcpy(message, buffer, sizeof(message));
      xfree(buffer);

   } else {

      message_id = atoi(path);

      if (message_id == 0) {

         /* return config */
         load_config_section(lbs->name, &buffer, error_str);
         if (error_str[0]) {
            rsprintf("Error loading configuration file: %s", error_str);
            return EL_FILE_ERROR;
         }

         size = strlen(buffer);
         strlcpy(message, buffer, sizeof(message));
         xfree(buffer);

      } else {

         /* return entry */

         for (index = 0; index < *lbs->n_el_index; index++)
            if (lbs->el_index[index].message_id == message_id)
               break;

         if (index == *lbs->n_el_index)
            return EL_NO_MSG;

         sprintf(file_name, "%s%s", lbs->data_dir, lbs->el_index[index].file_name);
         fh = open(file_name, O_RDWR | O_BINARY, 0644);
         if (fh < 0)
            return EL_FILE_ERROR;

         lseek(fh, lbs->el_index[index].offset, SEEK_SET);
         i = read(fh, message, sizeof(message) - 1);
         if (i <= 0) {
            close(fh);
            return EL_FILE_ERROR;
         }

         message[i] = 0;
         close(fh);

         /* decode message size */
         p = strstr(message + 8, "$@MID@$:");
         if (p == NULL)
            size = strlen(message);
         else
            size = (int) p - (int) message;

         message[size] = 0;
      }
   }

   show_plain_header(size, "export.txt");

   /* increase return buffer size if file too big */
   if (size > return_buffer_size - (int) strlen(return_buffer)) {
      delta = size - (return_buffer_size - strlen(return_buffer)) + 1000;

      return_buffer = xrealloc(return_buffer, return_buffer_size + delta);
      memset(return_buffer + return_buffer_size, 0, delta);
      return_buffer_size += delta;
   }

   return_length = strlen(return_buffer) + size;
   strlcat(return_buffer, message, return_buffer_size);

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

int download_config()
{
   char error_str[256];
   int size, delta;
   char message[TEXT_SIZE + 1000], *buffer;

   /* return complete config file */
   load_config_section(NULL, &buffer, error_str);
   if (error_str[0]) {
      rsprintf("Error loading configuration file: %s", error_str);
      return EL_FILE_ERROR;
   }

   size = strlen(buffer);
   strlcpy(message, buffer, sizeof(message));
   xfree(buffer);

   show_plain_header(size, "export.txt");

   /* increase return buffer size if file too big */
   if (size > return_buffer_size - (int) strlen(return_buffer)) {
      delta = size - (return_buffer_size - strlen(return_buffer)) + 1000;

      return_buffer = xrealloc(return_buffer, return_buffer_size + delta);
      memset(return_buffer + return_buffer_size, 0, delta);
      return_buffer_size += delta;
   }

   return_length = strlen(return_buffer) + size;
   strlcat(return_buffer, message, return_buffer_size);

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

void show_import_page(LOGBOOK * lbs)
{
   char str[256], str2[256];

   /*---- header ----*/

   show_html_header(lbs, FALSE, loc("ELOG CSV import"), TRUE, FALSE, NULL);

   rsprintf("<body><form method=\"POST\" action=\"./\" enctype=\"multipart/form-data\">\n");

   /*---- title ----*/

   show_standard_title(lbs->name, "", 0);

   /*---- menu buttons ----*/

   rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");

   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Cancel"));
   rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Import"));

   rsprintf("</span></td></tr>\n\n");

   /* table for two-column items */
   rsprintf("<tr><td class=\"form2\">");
   rsprintf("<table width=\"100%%\" cellspacing=0>\n");

   /*---- entry form ----*/

   rsprintf("<tr><td class=\"attribname\" nowrap width=\"10%%\">%s:</td>\n", loc("Field separator"));
   rsprintf("<td class=\"attribvalue\">");

   str[0] = 0;
   if (isparam("sep"))
      strcpy(str, getparam("sep"));

   if (str[0] == 0)
      rsprintf("<input type=\"radio\" checked id=\"comma\" name=\"sep\" value=\"auto\">");
   else
      rsprintf("<input type=\"radio\" id=\"comma\" name=\"sep\" value=\"auto\">");
   rsprintf("<label for=\"comma\">%s</label>\n", loc("Auto detect"));

   if (str[0] == ',')
      rsprintf("<input type=\"radio\" checked id=\"comma\" name=\"sep\" value=\",\">");
   else
      rsprintf("<input type=\"radio\" id=\"comma\" name=\"sep\" value=\",\">");
   rsprintf("<label for=\"comma\">%s (,)</label>\n", loc("Comma"));

   if (str[0] == ';')
      rsprintf("<input type=\"radio\" checked id=\"semi\" name=\"sep\" value=\";\">");
   else
      rsprintf("<input type=\"radio\" id=\"semi\" name=\"sep\" value=\";\">");
   rsprintf("<label for=\"semi\">%s (;)</label>\n", loc("Semicolon"));

   rsprintf("</td></tr>\n");

   rsprintf("<tr><td class=\"attribname\" nowrap width=\"10%%\">%s:</td>\n", loc("Options"));
   rsprintf("<td class=\"attribvalue\">");

   if (isparam("head"))
      rsprintf("<input type=checkbox checked id=\"head\" name=\"head\" value=\"1\">\n");
   else
      rsprintf("<input type=checkbox id=\"head\" name=\"head\" value=\"1\">\n");
   rsprintf("<label for=\"head\">%s</label><br>\n", loc("Derive attributes from CSV file"));

   if (isparam("ignore"))
      rsprintf("<input type=checkbox checked id=\"ignore\" name=\"ignore\" value=\"1\">\n");
   else
      rsprintf("<input type=checkbox id=\"ignore\" name=\"ignore\" value=\"1\">\n");
   rsprintf("<label for=\"ignore\">%s</label><br>\n", loc("Ignore first line"));

   rsprintf("<input type=checkbox id=\"preview\" name=\"preview\" value=\"1\">\n");
   rsprintf("<label for=\"preview\">%s</label><br>\n", loc("Preview import"));

   if (isparam("filltext"))
      rsprintf("<input type=checkbox checked id=\"filltext\" name=\"filltext\" value=\"1\">\n");
   else
      rsprintf("<input type=checkbox id=\"filltext\" name=\"filltext\" value=\"1\">\n");
   strcpy(str, loc("text"));
   sprintf(str2, loc("Column header '%s' must be present in CSV file"), str);
   rsprintf("<label for=\"filltext\">%s (%s)</label><br>\n", loc("Fill text body"), str2);

   rsprintf("</td></tr>\n");

   rsprintf("<tr><td class=\"attribname\" nowrap width=\"10%%\">%s:</td>\n", loc("CSV filename"));

   rsprintf("<td class=\"attribvalue\">");

   if (isparam("csvfile"))
      rsprintf("<b>%s</b>:<br>\n", loc("Please re-enter filename"));

   rsprintf("<input type=\"file\" size=\"60\" maxlength=\"200\" name=\"csvfile\" ");
   if (isparam("csvfile"))
      rsprintf(" value=\"%s\" ", getparam("csvfile"));
   rsprintf("accept=\"filetype/*\"></td></tr>\n");

   rsprintf("</table></td></tr></table>\n\n");
   show_bottom_text(lbs);
   rsprintf("</form></body></html>\r\n");

}

/*------------------------------------------------------------------*/

void csv_import(LOGBOOK * lbs, char *csv, char *csvfile)
{
   char *list, *line, *p, str[256], date[80], sep[80];
   int i, j, n, n_attr, iline, n_imported, textcol;
   BOOL first, in_quotes, filltext;

   list = xmalloc(MAX_N_ATTR * NAME_LENGTH);
   line = xmalloc(10000);

   first = TRUE;
   in_quotes = FALSE;
   iline = n_imported = 0;
   filltext = FALSE;
   textcol = -1;

   strcpy(sep, ",");
   if (isparam("sep"))
      strcpy(sep, getparam("sep"));
   if (sep[0] == 0)
      strcpy(sep, ",");
   if (strieq(sep, "auto")) {

      /* count commas */
      for (i = 0, p = csv; p; i++) {
         p = strchr(p, ',');
         if (p)
            p++;
      }
      n = i;

      /* count semicolon */
      for (i = 0, p = csv; p; i++) {
         p = strchr(p, ';');
         if (p)
            p++;
      }

      strcpy(sep, i > n ? ";" : ",");
   }

   n_attr = lbs->n_attr;

   if (isparam("preview")) {

      /* title row */
      sprintf(str, loc("CSV import preview of %s"), csvfile);
      show_standard_header(lbs, TRUE, str, "./", FALSE, NULL);
      rsprintf("<table class=\"frame\" cellpadding=0 cellspacing=0>\n");
      rsprintf("<tr><td class=\"title1\">%s</td></tr>\n", str, str);

      /* menu buttons */
      rsprintf("<tr><td class=\"menuframe\"><span class=\"menu1\">\n");
      rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("Cancel"));
      rsprintf("<input type=submit name=cmd value=\"%s\">\n", loc("CSV Import"));

      /* hidden fields */
      rsprintf("<input type=hidden name=sep value=\"%s\">\n", sep);
      if (isparam("head"))
         rsprintf("<input type=hidden name=head value=\"%s\">\n", getparam("head"));
      if (isparam("ignore"))
         rsprintf("<input type=hidden name=ignore value=\"%s\">\n", getparam("ignore"));
      if (isparam("filltext"))
         rsprintf("<input type=hidden name=filltext value=\"%s\">\n", getparam("filltext"));
      rsprintf("<input type=hidden name=csvfile value=\"%s\">\n", csvfile);

      rsprintf("</span></td></tr>\n\n");

      rsprintf("<tr><td><table class=\"listframe\" width=\"100%%\" cellspacing=0>");
   }

   p = csv;

   do {
      for (i = 0; i < 10000 && *p; i++) {
         if (!in_quotes && (*p == '\r' || *p == '\n'))
            break;

         line[i] = *p++;
         if (line[i] == '"')
            in_quotes = !in_quotes;
      }

      line[i] = 0;
      while (*p == '\r' || *p == '\n')
         p++;
      if (!*p)
         break;

      memset(list, 0, MAX_N_ATTR * NAME_LENGTH);
      n = strbreak(line, (char (*)[NAME_LENGTH]) list, MAX_N_ATTR, sep);

      if (n == MAX_N_ATTR) {
         sprintf(str, loc("Too many attributes in CSV file"));
         show_error(str);
      }

      /* ignore first line */
      if (first && isparam("ignore")) {
         first = FALSE;
         continue;
      }

      /* check if text column is present */
      if (first && atoi(getparam("filltext"))) {
         for (i = 0; i < n; i++)
            if (strieq(list + i * NAME_LENGTH, loc("text"))) {
               filltext = TRUE;
               textcol = i;
               break;
            }
      }

      /* derive attributes from first line */
      if (first && isparam("head")) {
         if (isparam("preview")) {
            rsprintf("<tr>\n");
            for (i = 0; i < n; i++)
               if (i != textcol)
                  rsprintf("<th class=\"listtitle\">%s</th>\n", list + i * NAME_LENGTH);

            if (filltext)
               rsprintf("<th class=\"listtitle\">%s</th>\n", loc("text"));

            rsprintf("</tr>\n");

            if (filltext)
               n_attr = n - 1;
            else
               n_attr = n;

         } else {
            for (i = j = 0; i < n; i++)
               if (i != textcol)
                  strlcpy(attr_list[j++], list + i * NAME_LENGTH, NAME_LENGTH);

            if (filltext) {
               if (!set_attributes(lbs, attr_list, n - 1))
                  return;
               lbs->n_attr = n - 1;
            } else {
               if (!set_attributes(lbs, attr_list, n))
                  return;
               lbs->n_attr = n;
            }
         }

      } else {

         if (isparam("preview")) {
            rsprintf("<tr>\n");
            for (i = j = 0; i < n_attr; i++) {
               if (iline % 2 == 0)
                  rsputs("<td class=\"list1\">");
               else
                  rsputs("<td class=\"list2\">");

               /* skip text column */
               if (i == textcol)
                  j++;

               if (i >= n || !list[j * NAME_LENGTH])
                  rsputs("&nbsp;");
               else
                  rsputs(list + j * NAME_LENGTH);

               rsputs("</td>\n");
               j++;
            }

            if (filltext) {
               rsputs("<td class=\"summary\">");
               if (list[textcol * NAME_LENGTH])
                  rsputs(list + textcol * NAME_LENGTH);
               else
                  rsputs("&nbsp;");
               rsputs("</td>\n");
            }

            rsputs("</tr>\n");
            iline++;

         } else {

            if (!filltext) {
               /* submit entry */
               date[0] = 0;
               if (el_submit(lbs, 0, FALSE, date, attr_list, (char (*)[NAME_LENGTH]) list,
                             n_attr, "", "", "", "plain", NULL, TRUE, NULL))
                  n_imported++;
            } else {
               strlcpy(line, list + textcol * NAME_LENGTH, 10000);
               insert_breaks(line, 78, 10000);

               for (i = textcol; i < n_attr; i++)
                  strlcpy(list + i * NAME_LENGTH, list + (i + 1) * NAME_LENGTH, NAME_LENGTH);

               /* submit entry */
               date[0] = 0;
               if (el_submit(lbs, 0, FALSE, date, attr_list, (char (*)[NAME_LENGTH]) list,
                             n_attr, line, "", "", "plain", NULL, TRUE, NULL))
                  n_imported++;
            }
         }
      }

      first = FALSE;

   } while (*p);

   xfree(line);
   xfree(list);

   if (isparam("preview")) {
      rsprintf("</table></td></tr></table>\n");
      show_bottom_text(lbs);
      rsprintf("</form></body></html>\r\n");

      return;
   }

   sprintf(str, loc("%d entries successfully imported"), n_imported);
   show_elog_list(lbs, 0, 0, 1, str);
}

/*------------------------------------------------------------------*/

int show_md5_page(LOGBOOK * lbs)
{
   int i, j;
   char *buffer, error_str[256];
   unsigned char digest[16];

   /* header */
   rsprintf("HTTP/1.1 200 Document follows\r\n");
   rsprintf("Server: ELOG HTTP %s\r\n", VERSION);
   rsprintf("Accept-Ranges: bytes\r\n");
   rsprintf("Connection: close\r\n");
   rsprintf("Content-Type: text/plain;charset=%s\r\n", DEFAULT_HTTP_CHARSET);
   rsprintf("Pragma: no-cache\r\n");
   rsprintf("Expires: Fri, 01 Jan 1983 00:00:00 GMT\r\n\r\n");

   /* calculate MD5 for logbook section in config file */
   load_config_section(lbs->name, &buffer, error_str);
   if (error_str[0])
      rsprintf("Error loading configuration file: %s", error_str);
   else {
      rsprintf("ID: %6d MD5:", 0);

      remove_crlf(buffer);
      MD5_checksum(buffer, strlen(buffer), digest);

      for (i = 0; i < 16; i++)
         rsprintf("%02X", digest[i]);
      rsprintf("\n");
   }
   xfree(buffer);

   /* show MD5's of logbook entries */
   for (i = 0; i < *lbs->n_el_index; i++) {
      rsprintf("ID: %6d MD5:", lbs->el_index[i].message_id);
      for (j = 0; j < 16; j++)
         rsprintf("%02X", lbs->el_index[i].md5_digest[j]);
      rsprintf("\n");
   }

   keep_alive = 0;

   return EL_SUCCESS;
}

/*------------------------------------------------------------------*/

void combine_url(LOGBOOK * lbs, char *url, char *param, char *result, int size)
{

   if (strstr(url, "http://"))
      strlcpy(result, url + 7, size);
   else if (strstr(url, "https://"))
      strlcpy(result, url + 8, size);
   else
      strlcpy(result, url, size);

   url_encode(result, size);

   if (result[strlen(result) - 1] != '/')
      strlcat(result, "/", size);

   if (lbs != NULL) {
      if (!strstr(result, lbs->name_enc)) {
         strlcat(result, lbs->name_enc, size);
         strlcat(result, "/", size);
      }
   }

   if (param)
      strlcat(result, param, size);
}

/*------------------------------------------------------------------*/

int retrieve_remote_md5(LOGBOOK * lbs, char *host, MD5_INDEX ** md5_index, char *error_str)
{
   int i, n, id, x, version;
   char *text, *p, url[256], str[1000];

   *md5_index = NULL;

   combine_url(lbs, host, "?cmd=GetMD5", url, sizeof(url));

   text = NULL;
   error_str[0] = 0;
   if (retrieve_url(url, &text, NULL) < 0) {
      sprintf(error_str, loc("Cannot connect to remote server \"%s\""), host);
      return -1;
   }
   p = strstr(text, "ELOG HTTP ");
   if (!p) {
      if (isparam("debug"))
         rsputs(text);
      sprintf(error_str, loc("Remote server is not an ELOG server"));
      xfree(text);
      return -1;
   }
   version = atoi(p + 10) * 100 + atoi(p + 12) * 10 + atoi(p + 14);
   if (version < 250) {
      if (isparam("debug"))
         rsputs(text);
      memset(str, 0, sizeof(str));
      strncpy(str, p + 10, 5);
      sprintf(error_str, loc("Incorrect remote ELOG server version %s"), str);
      xfree(text);
      return -1;
   }

   p = strstr(text, "Location: ");
   if (p) {
      if (isparam("debug"))
         rsputs(text);

      if (strstr(text, "?fail="))
         sprintf(error_str, loc("Invalid user name \"%s\" or password for remote logbook"), getparam("unm"));
      else {
         strlcpy(str, p + 9, sizeof(str));
         if (strchr(str, '?'))
            *strchr(str, '?') = 0;
         strcpy(error_str, loc("URL is redirected to:"));
         strcat(error_str, str);
      }

      return -3;
   }

   p = strstr(text, "\r\n\r\n");
   if (!p) {
      if (isparam("debug"))
         rsputs(text);
      sprintf(error_str, loc("Invalid HTTP header"));
      xfree(text);
      return -1;
   }

   for (n = 0;; n++) {
      p = strstr(p, "ID:");
      if (!p)
         break;
      p += 3;

      id = atoi(p);

      p = strstr(p, "MD5:");
      if (!p)
         break;
      p += 4;

      if (n == 0)
         *md5_index = xmalloc(sizeof(MD5_INDEX));
      else
         *md5_index = xrealloc(*md5_index, (n + 1) * sizeof(MD5_INDEX));

      (*md5_index)[n].message_id = id;

      for (i = 0; i < 16; i++) {
         sscanf(p + 2 * i, "%02X", &x);
         (*md5_index)[n].md5_digest[i] = (unsigned char) x;
      }
   }

   if (n == 0) {
      if (isparam("debug"))
         rsputs(text);
      if (strstr(text, "Login")) {
         sprintf(error_str, loc("No user name supplied to access remote logbook"));
         xfree(text);
         return -2;
      } else
         sprintf(error_str, loc("Error accessing remote logbook"));
   }

   xfree(text);

   return n;
}

/*------------------------------------------------------------------*/

INT send_tcp(int sock, char *buffer, unsigned int buffer_size, INT flags)
/********************************************************************\

    Send network data over TCP port. Break buffer in smaller
    parts if larger than maximum TCP buffer size (usually 64k).

\********************************************************************/
{
#ifndef NET_TCP_SIZE
#define NET_TCP_SIZE 65536
#endif

   unsigned int count;
   int status;

   /* transfer fragments until complete buffer is transferred */

   for (count = 0; count < buffer_size - NET_TCP_SIZE;) {
      status = send(sock, buffer + count, NET_TCP_SIZE, flags);
      if (status != -1)
         count += status;
      else {
         return status;
      }
   }

   while (count < buffer_size) {
      status = send(sock, buffer + count, buffer_size - count, flags);
      if (status != -1)
         count += status;
      else {
         return status;
      }
   }

   return count;
}

/*------------------------------------------------------------------*/

int submit_message(LOGBOOK * lbs, char *host, int message_id, char *error_str)
{
   int size, i, n, status, fh, port, sock, content_length, header_length, remote_id, n_attr;
   char str[256], file_name[MAX_PATH_LENGTH], attrib[MAX_N_ATTR][NAME_LENGTH];
   char subdir[256], param[256], remote_host_name[256], url[256];
   char date[80], *text, in_reply_to[80], reply_to[MAX_REPLY_TO * 10],
       attachment[MAX_ATTACHMENTS][MAX_PATH_LENGTH], encoding[80], locked_by[256], *buffer;
   char *content, *p, boundary[80], request[10000], response[10000];
   struct hostent *phe;
   struct sockaddr_in bind_addr;

   text = xmalloc(TEXT_SIZE);
   error_str[0] = 0;

   /* get message with attribute list devied from database */
   size = TEXT_SIZE;
   status =
       el_retrieve(lbs, message_id, date, attr_list, attrib, -1,
                   text, &size, in_reply_to, reply_to, attachment, encoding, locked_by);

   if (status != EL_SUCCESS) {
      xfree(text);
      strcpy(error_str, loc("Cannot read entry from local logbook"));
      return -1;
   }

   /* count attributes */
   for (n_attr = 0; attr_list[n_attr][0]; n_attr++);

   combine_url(lbs, host, "", url, sizeof(url));
   split_url(url, remote_host_name, &port, subdir, param);

   /* create socket */
   if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
      xfree(text);
      strcpy(error_str, loc("Cannot create socket"));
      return -1;
   }

   /* compose remote address */
   memset(&bind_addr, 0, sizeof(bind_addr));
   bind_addr.sin_family = AF_INET;
   bind_addr.sin_addr.s_addr = 0;
   bind_addr.sin_port = htons((unsigned short) port);

   phe = gethostbyname(remote_host_name);
   if (phe == NULL) {
      closesocket(sock);
      xfree(text);
      sprintf(error_str, loc("Cannot resolve host name \"%s\""), remote_host_name);
      return -1;
   }
   memcpy((char *) &(bind_addr.sin_addr), phe->h_addr, phe->h_length);

   /* connect to server */
   status = connect(sock, (void *) &bind_addr, sizeof(bind_addr));
   if (status != 0) {
      closesocket(sock);
      xfree(text);
      sprintf(error_str, loc("Cannot connect to host %s, port %d"), remote_host_name, port);
      return -1;
   }

   content_length = 100000;
   for (i = 0; i < MAX_ATTACHMENTS; i++)
      if (attachment[i][0]) {
         strlcpy(file_name, lbs->data_dir, sizeof(file_name));
         strlcat(file_name, attachment[i], sizeof(file_name));

         fh = open(file_name, O_RDONLY | O_BINARY);
         if (fh > 0) {
            lseek(fh, 0, SEEK_END);
            size = TELL(fh);
            close(fh);
         } else
            size = 0;

         content_length += size;
      }

   content = xmalloc(content_length);

   /* compose content */
   srand((unsigned) time(NULL));
   sprintf(boundary, "---------------------------%04X%04X%04X", rand(), rand(), rand());
   strcpy(content, boundary);
   strcat(content, "\r\nContent-Disposition: form-data; name=\"cmd\"\r\n\r\nSubmit\r\n");

   sprintf(content + strlen(content),
           "%s\r\nContent-Disposition: form-data; name=\"mirror_id\"\r\n\r\n%d\r\n", boundary, message_id);

   if (isparam("unm"))
      sprintf(content + strlen(content),
              "%s\r\nContent-Disposition: form-data; name=\"unm\"\r\n\r\n%s\r\n", boundary, getparam("unm"));

   if (isparam("upwd"))
      sprintf(content + strlen(content),
              "%s\r\nContent-Disposition: form-data; name=\"upwd\"\r\n\r\n%s\r\n",
              boundary, getparam("upwd"));

   if (in_reply_to[0])
      sprintf(content + strlen(content),
              "%s\r\nContent-Disposition: form-data; name=\"in_reply_to\"\r\n\r\n%s\r\n",
              boundary, in_reply_to);

   if (reply_to[0])
      sprintf(content + strlen(content),
              "%s\r\nContent-Disposition: form-data; name=\"reply_to\"\r\n\r\n%s\r\n", boundary, reply_to);

   for (i = 0; i < n_attr; i++)
      sprintf(content + strlen(content),
              "%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n", boundary,
              attr_list[i], attrib[i]);

   sprintf(content + strlen(content),
           "%s\r\nContent-Disposition: form-data; name=\"entry_date\"\r\n\r\n%s\r\n", boundary, date);

   sprintf(content + strlen(content),
           "%s\r\nContent-Disposition: form-data; name=\"encoding\"\r\n\r\n%s\r\n", boundary, encoding);

   sprintf(content + strlen(content),
           "%s\r\nContent-Disposition: form-data; name=\"Text\"\r\n\r\n%s\r\n%s\r\n",
           boundary, text, boundary);

   content_length = strlen(content);
   p = content + content_length;

   /* read attachments */
   for (i = 0; i < MAX_ATTACHMENTS; i++)
      if (attachment[i][0]) {
         strlcpy(file_name, lbs->data_dir, sizeof(file_name));
         strlcat(file_name, attachment[i], sizeof(file_name));

         fh = open(file_name, O_RDONLY | O_BINARY);
         if (fh > 0) {
            lseek(fh, 0, SEEK_END);
            size = TELL(fh);
            lseek(fh, 0, SEEK_SET);
            buffer = xmalloc(size);
            read(fh, buffer, size);
            close(fh);

            /* submit attachment */
            sprintf(p,
                    "Content-Disposition: form-data; name=\"attfile%d\"; filename=\"%s\"\r\n\r\n",
                    i + 1, attachment[i]);

            content_length += strlen(p);
            p += strlen(p);

            memcpy(p, buffer, size);
            p += size;
            strcpy(p, boundary);
            strcat(p, "\r\n");

            content_length += size + strlen(p);
            p += strlen(p);

            xfree(buffer);
         }
      }

   /* compose request */
   strcpy(request, "POST ");
   if (subdir[0]) {
      if (subdir[0] != '/')
         strcat(request, "/");
      strcat(request, subdir);
      if (request[strlen(request) - 1] != '/')
         strcat(request, "/");
   }
   strcat(request, " HTTP/1.0\r\n");

   sprintf(request + strlen(request), "Content-Type: multipart/form-data; boundary=%s\r\n", boundary);
   sprintf(request + strlen(request), "Host: %s\r\n", host_name);
   sprintf(request + strlen(request), "User-Agent: ELOGD\r\n");
   sprintf(request + strlen(request), "Content-Length: %d\r\n", content_length);

   if (isparam("wpwd"))
      sprintf(request + strlen(request), "Cookie: wpwd=%s\r\n", getparam("wpwd"));

   strcat(request, "\r\n");

   header_length = strlen(request);

   /* send request */
   send(sock, request, header_length, 0);

   /* send content */
   send_tcp(sock, content, content_length, 0);

   /* receive response */
   i = recv(sock, response, 10000, 0);
   if (i < 0) {
      closesocket(sock);
      xfree(text);
      strcpy(error_str, "Cannot receive response");
      return -1;
   }

   /* discard remainder of response */
   n = i;
   while (i > 0) {
      i = recv(sock, response + n, 10000, 0);
      if (i > 0)
         n += i;
   }
   response[n] = 0;

   closesocket(sock);
   remote_id = -1;

   /* check response status */
   if (strstr(response, "302 Found")) {
      if (strstr(response, "Location:")) {
         if (strstr(response, "fail"))
            sprintf(error_str, "Invalid user name or password\n");

         strlcpy(str, strstr(response, "Location:") + 9, sizeof(str));
         if (strchr(str, '\n'))
            *strchr(str, '\n') = 0;
         if (strchr(str, '?'))
            *strchr(str, '?') = 0;

         if (strrchr(str, '/'))
            remote_id = atoi(strrchr(str, '/') + 1);
         else
            remote_id = atoi(str);
      }
   } else if (strstr(response, "Logbook Selection"))
      sprintf(error_str, "No logbook specified\n");
   else if (strstr(response, "enter password"))
      sprintf(error_str, "Missing or invalid password\n");
   else if (strstr(response, "form name=form1"))
      sprintf(error_str, "Missing or invalid user name/password\n");
   else if (strstr(response, "Error: Attribute")) {
      strncpy(str, strstr(response, "Error: Attribute") + 20, sizeof(str));
      if (strchr(str, '<'))
         *strchr(str, '<') = 0;
      sprintf(error_str, "Missing required attribute \"%s\"\n", str);
   } else
      sprintf(error_str, "Error transmitting message\n");

   if (error_str[0] && isparam("debug"))
      rsputs(response);

   xfree(text);

   if (error_str[0])
      return -1;

   return remote_id;
}

/*------------------------------------------------------------------*/

int receive_message(LOGBOOK * lbs, char *url, int message_id, char *error_str, BOOL bnew)
{
   int i, status, size, n_attr, header_size;
   char str[NAME_LENGTH], str2[NAME_LENGTH], *p, *p2, *message, date[80],
       attrib[MAX_N_ATTR][NAME_LENGTH], in_reply_to[80], reply_to[MAX_REPLY_TO * 10],
       encoding[80], locked_by[256], attachment[MAX_ATTACHMENTS][MAX_PATH_LENGTH],
       attachment_all[64 * MAX_ATTACHMENTS];

   error_str[0] = 0;

   combine_url(lbs, url, "", str, sizeof(str));
   sprintf(str + strlen(str), "%d?cmd=%s", message_id, loc("Download"));

   retrieve_url(str, &message, NULL);
   if (message == NULL) {
      sprintf(error_str, loc("Cannot receive \"%s\""), str);
      return -1;
   }
   p = strstr(message, "\r\n\r\n");
   if (p == NULL) {
      if (isparam("debug"))
         rsputs(message);
      xfree(message);
      sprintf(error_str, loc("Cannot receive \"%s\""), str);
      return -1;
   }
   p += 4;

   /* check for correct ID */
   if (atoi(p + 8) != message_id) {
      if (isparam("debug"))
         rsputs(message);
      xfree(message);
      sprintf(error_str, loc("Received wrong entry id \"%d\""), atoi(p + 8));
      return -1;
   }

   /* decode entry */
   el_decode(p, "Date: ", date);
   el_decode(p, "Reply to: ", reply_to);
   el_decode(p, "In reply to: ", in_reply_to);

   /* derive attribute names from message */
   for (i = 0;; i++) {
      el_enum_attr(p, i, attr_list[i], attrib[i]);
      if (!attr_list[i][0])
         break;
   }
   n_attr = i;

   el_decode(p, "Attachment: ", attachment_all);
   el_decode(p, "Encoding: ", encoding);

   /* break apart attachements */
   for (i = 0; i < MAX_ATTACHMENTS; i++)
      attachment[i][0] = 0;

   for (i = 0; i < MAX_ATTACHMENTS; i++) {
      if (i == 0)
         p2 = strtok(attachment_all, ",");
      else
         p2 = strtok(NULL, ",");

      if (p2 != NULL)
         strcpy(attachment[i], p2);
      else
         break;
   }

   el_decode(p, "Locked by: ", locked_by);
   if (locked_by[0]) {
      xfree(message);
      sprintf(error_str, loc("Entry #%d is locked on remote server"), message_id);
      return -1;
   }

   p = strstr(message, "========================================\n");

   /* check for \n -> \r conversion (e.g. zipping/unzipping) */
   if (p == NULL)
      p = strstr(message, "========================================\r");

   if (p != NULL) {
      p += 41;

      /* remove last CR */
      if (p[strlen(p) - 1] == '\n')
         p[strlen(p) - 1] = 0;

      status = el_submit(lbs, message_id, !bnew, date, attr_list,
                         attrib, n_attr, p, in_reply_to, reply_to, encoding, attachment, FALSE, "");

      xfree(message);

      if (status != message_id) {
         sprintf(error_str, loc("Cannot save remote entry locally"));
         return -1;
      }

      for (i = 0; i < MAX_ATTACHMENTS; i++) {
         if (attachment[i][0]) {

            combine_url(lbs, url, "", str, sizeof(str));
            strlcpy(str2, attachment[i], sizeof(str2));
            str2[13] = '/';
            strlcat(str, str2, sizeof(str));

            size = retrieve_url(str, &message, NULL);
            p = strstr(message, "\r\n\r\n");
            if (p == NULL) {
               xfree(message);
               sprintf(error_str, loc("Cannot receive \"%s\""), str);
               return -1;
            }
            p += 4;
            header_size = (int)p - (int)message;

            el_submit_attachment(lbs, attachment[i], p, size - header_size, NULL);
            xfree(message);
         }
      }
   } else {
      xfree(message);
      return -1;
   }

   return 1;
}

/*------------------------------------------------------------------*/

void submit_config(LOGBOOK * lbs, char *server, char *buffer, char *error_str)
{
   int i, n, status, port, sock, content_length, header_length;
   char str[256];
   char subdir[256], param[256], remote_host_name[256];
   char *content, *p, boundary[80], request[10000], response[10000];
   struct hostent *phe;
   struct sockaddr_in bind_addr;

   error_str[0] = 0;

   combine_url(lbs, server, "", str, sizeof(str));
   split_url(str, remote_host_name, &port, subdir, param);

   /* create socket */
   if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
      strcpy(error_str, loc("Cannot create socket"));
      return;
   }

   /* compose remote address */
   memset(&bind_addr, 0, sizeof(bind_addr));
   bind_addr.sin_family = AF_INET;
   bind_addr.sin_addr.s_addr = 0;
   bind_addr.sin_port = htons((unsigned short) port);

   phe = gethostbyname(remote_host_name);
   if (phe == NULL) {
      closesocket(sock);
      sprintf(error_str, loc("Cannot resolve host name \"%s\""), remote_host_name);
      return;
   }
   memcpy((char *) &(bind_addr.sin_addr), phe->h_addr, phe->h_length);

   /* connect to server */
   status = connect(sock, (void *) &bind_addr, sizeof(bind_addr));
   if (status != 0) {
      closesocket(sock);
      sprintf(error_str, loc("Cannot connect to host %s, port %d"), remote_host_name, port);
      return;
   }

   content_length = 100000;
   content = xmalloc(content_length);

   /* compose content */
   srand((unsigned) time(NULL));
   sprintf(boundary, "---------------------------%04X%04X%04X", rand(), rand(), rand());
   strcpy(content, boundary);
   strcat(content, "\r\nContent-Disposition: form-data; name=\"cmd\"\r\n\r\nSave\r\n");

   if (isparam("unm"))
      sprintf(content + strlen(content),
              "%s\r\nContent-Disposition: form-data; name=\"unm\"\r\n\r\n%s\r\n", boundary, getparam("unm"));

   if (isparam("upwd"))
      sprintf(content + strlen(content),
              "%s\r\nContent-Disposition: form-data; name=\"upwd\"\r\n\r\n%s\r\n",
              boundary, getparam("upwd"));

   sprintf(content + strlen(content),
           "%s\r\nContent-Disposition: form-data; name=\"Text\"\r\n\r\n%s\r\n%s\r\n",
           boundary, buffer, boundary);

   content_length = strlen(content);
   p = content + content_length;

   /* compose request */
   strcpy(request, "POST ");
   if (subdir[0]) {
      if (subdir[0] != '/')
         strcat(request, "/");
      strcat(request, subdir);
      if (request[strlen(request) - 1] != '/')
         strcat(request, "/");
   }
   strcat(request, " HTTP/1.0\r\n");

   sprintf(request + strlen(request), "Content-Type: multipart/form-data; boundary=%s\r\n", boundary);
   sprintf(request + strlen(request), "Host: %s\r\n", host_name);
   sprintf(request + strlen(request), "User-Agent: ELOGD\r\n");
   sprintf(request + strlen(request), "Content-Length: %d\r\n", content_length);

   if (isparam("wpwd"))
      sprintf(request + strlen(request), "Cookie: wpwd=%s\r\n", getparam("wpwd"));

   strcat(request, "\r\n");

   header_length = strlen(request);

   /* send request */
   send(sock, request, header_length, 0);

   /* send content */
   send(sock, content, content_length, 0);

   /* receive response */
   i = recv(sock, response, 10000, 0);
   if (i < 0) {
      closesocket(sock);
      strcpy(error_str, "Cannot receive response");
      return;
   }

   /* discard remainder of response */
   n = i;
   while (i > 0) {
      i = recv(sock, response + n, 10000, 0);
      if (i > 0)
         n += i;
   }
   response[n] = 0;

   closesocket(sock);

   /* check response status */
   if (strstr(response, "302 Found")) {
      if (strstr(response, "Location:")) {
         if (strstr(response, "fail"))
            sprintf(error_str, "Invalid usr name or password\n");
      }
   } else if (strstr(response, "Logbook Selection"))
      sprintf(error_str, "No logbook specified\n");
   else if (strstr(response, "enter password"))
      sprintf(error_str, "Missing or invalid password\n");
   else if (strstr(response, "form name=form1"))
      sprintf(error_str, "Missing or invalid user name/password\n");
   else if (strstr(response, "Error: Attribute")) {
      strncpy(str, strstr(response, "Error: Attribute") + 20, sizeof(str));
      if (strchr(str, '<'))
         *strchr(str, '<') = 0;
      sprintf(error_str, "Missing required attribute \"%s\"\n", str);
   } else
      sprintf(error_str, "Error transmitting message\n");
}

/*------------------------------------------------------------------*/

void receive_config(LOGBOOK * lbs, char *server, char *error_str)
{
   char str[256], pwd[256], *buffer, *p;
   int status, version;

   error_str[0] = pwd[0] = 0;

   do {

      combine_url(lbs, server, "", str, sizeof(str));
      if (lbs == NULL)
         strcat(str, "?cmd=GetConfig"); // request complete config file
      else
         strcat(str, "?cmd=Download");  // request config section of logbook

      if (retrieve_url(str, &buffer, pwd) < 0) {
         *strchr(str, '?') = 0;
         sprintf(error_str, "Cannot contact elogd server at http://%s", str);
         return;
      }

      /* check version */
      p = strstr(buffer, "ELOG HTTP ");
      if (!p) {
         if (verbose)
            puts(buffer);
         sprintf(error_str, "Remote server is not an ELOG server");
         xfree(buffer);
         return;
      }
      version = atoi(p + 10) * 100 + atoi(p + 12) * 10 + atoi(p + 14);
      if (version < 254) {
         if (verbose)
            puts(buffer);

         strlcpy(str, p + 10, 10);
         if (strchr(str, '\r'))
            *strchr(str, '\r') = 0;

         sprintf(error_str, "Incorrect remote ELOG server version %s, must be 2.5.4 or later", str);
         xfree(buffer);
         return;
      }

      /* evaluate status */
      p = strchr(buffer, ' ');
      if (p == NULL) {
         if (verbose)
            puts(buffer);
         xfree(buffer);
         *strchr(str, '?') = 0;
         sprintf(error_str, "Received invalid response from elogd server at http://%s", str);
         xfree(buffer);
         return;
      }
      p++;
      status = atoi(p);
      if (status == 401) {
         if (verbose)
            puts(buffer);
         xfree(buffer);
         eprintf("Please enter password to access remote elogd server: ");
         fgets(pwd, sizeof(pwd), stdin);
         while (pwd[strlen(pwd) - 1] == '\n' || pwd[strlen(pwd) - 1] == '\r')
            pwd[strlen(pwd) - 1] = 0;
      } else if (status != 200) {
         if (verbose)
            puts(buffer);
         xfree(buffer);
         *strchr(str, '?') = 0;
         sprintf(error_str, "Received invalid response from elogd server at http://%s", str);
         return;
      }

   } while (status != 200);

   p = strstr(buffer, "\r\n\r\n");
   if (p == NULL) {
      if (verbose)
         puts(buffer);
      xfree(buffer);
      sprintf(error_str, loc("Cannot receive \"%s\""), str);
      return;
   }
   p += 4;

   if (lbs == NULL) {
      if (!save_config(p, str))
         rsprintf(str);
   } else {
      if (!save_admin_config(lbs->name, p, str))
         rsprintf(str);
   }

   xfree(buffer);
}

/*------------------------------------------------------------------*/

int adjust_config(char *url)
{
   int fh, i, length;
   char *buf, *buf2, *p1, *p2;
   char str[256];

   fh = open(config_file, O_RDWR | O_BINARY, 0644);
   if (fh < 0) {
      sprintf(str, loc("Cannot open file <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      eputs(str);
      return 0;
   }

   /* read previous contents */
   length = lseek(fh, 0, SEEK_END);
   lseek(fh, 0, SEEK_SET);
   buf = xmalloc(2 * length + 1000);
   read(fh, buf, length);
   buf[length] = 0;

   /* add mirror server */
   p1 = strstr(buf, "Mirror server =");
   if (p1 != NULL) {
      p2 = strchr(p1, '\n');
      if (p2 && *(p2 - 1) == '\r')
         p2--;
   } else {
      p1 = strstr(buf, "[global]");
      if (p1 == NULL) {
         eputs("Cannot find [global] section in config file.");
         return 0;
      }
      p1 = strchr(p1, '\n');
      while (*p1 == '\n' || *p1 == '\r')
         p1++;

      p2 = p1;
   }

   /* save tail */
   buf2 = NULL;
   if (p2)
      buf2 = xstrdup(p2);

   sprintf(p1, "Mirror server = %s\r\n", url);
   strlcat(p1, buf2, length + 1000);
   xfree(buf2);
   eprintf("Option \"Mirror server = %s\" added to config file.\n", url);

   /* outcomment "URL = xxx" */
   p1 = strstr(buf, "URL =");
   if (p1 != NULL) {

      /* save tail */
      buf2 = xstrdup(p1);

      /* add comment */
      sprintf(p1, "; Following line has been outcommented after cloning\r\n");
      strlcat(p1, "; ", length + 1000);
      strlcat(p1, buf2, length + 1000);
      xfree(buf2);

      eputs("Option \"URL = xxx\" has been outcommented from config file.");
   }

   adjust_crlf(buf, 2 * length + 1000);

   lseek(fh, 0, SEEK_SET);
   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(str, loc("Cannot write to <b>%s</b>"), config_file);
      strcat(str, ": ");
      strcat(str, strerror(errno));
      eputs(str);
      close(fh);
      xfree(buf);
      return 0;
   }

   TRUNCATE(fh);

   close(fh);
   xfree(buf);

   return 1;
}

/*------------------------------------------------------------------*/

void receive_pwdfile(LOGBOOK * lbs, char *server, char *error_str)
{
   char str[256], pwd[256], url[256], *buffer, *buf, *p;
   int i, status, version, fh;

   error_str[0] = pwd[0] = 0;

   do {

      combine_url(lbs, server, "", url, sizeof(url));
      strcpy(str, url);
      strcat(str, "?cmd=GetPwdFile");   // request password file

      if (retrieve_url(str, &buffer, pwd) < 0) {
         *strchr(str, '?') = 0;
         sprintf(error_str, "Cannot contact elogd server at http://%s", str);
         return;
      }

      /* check version */
      p = strstr(buffer, "ELOG HTTP ");
      if (!p) {
         sprintf(error_str, "Remote server is not an ELOG server");
         xfree(buffer);
         return;
      }
      version = atoi(p + 10) * 100 + atoi(p + 12) * 10 + atoi(p + 14);
      if (version < 254) {
         strlcpy(str, p + 10, 10);
         if (strchr(str, '\r'))
            *strchr(str, '\r') = 0;

         sprintf(error_str, "Incorrect remote ELOG server version %s, must be 2.5.4 or later", str);
         xfree(buffer);
         return;
      }

      /* evaluate status */
      p = strchr(buffer, ' ');
      if (p == NULL) {
         xfree(buffer);
         *strchr(str, '?') = 0;
         sprintf(error_str, "Received invalid response from elogd server at http://%s", str);
         xfree(buffer);
         return;
      }
      p++;
      status = atoi(p);
      if (status == 401) {
         xfree(buffer);
         eprintf("Please enter password to access remote elogd server: ");
         fgets(pwd, sizeof(pwd), stdin);
         while (pwd[strlen(pwd) - 1] == '\n' || pwd[strlen(pwd) - 1] == '\r')
            pwd[strlen(pwd) - 1] = 0;
      } else if (status != 200 && status != 302) {
         xfree(buffer);
         *strchr(str, '?') = 0;
         sprintf(error_str, "Received invalid response from elogd server at http://%s", str);
         return;
      }

      p = strstr(buffer, "\r\n\r\n");
      if (p == NULL) {
         xfree(buffer);
         sprintf(error_str, loc("Cannot receive \"%s\""), str);
         return;
      }
      p += 4;

      /* check for logbook access */
      if (strstr(p, loc("Please login")) || strstr(p, "GetPwdFile") || status == 302) {

         if (strstr(buffer, "?fail="))
            eprintf("\nInvalid username or password.");

         if (strstr(p, loc("Please login")) == NULL && strstr(p, "GetPwdFile") && isparam("unm"))
            eprintf("\nUser \"%s\" has no admin rights on remote server.", getparam("unm"));

         /* ask for username and password */
         eprintf("\nPlease enter admin username to access\n%s: ", url);
         fgets(str, sizeof(str), stdin);
         while (str[strlen(str) - 1] == '\r' || str[strlen(str) - 1] == '\n')
            str[strlen(str) - 1] = 0;
         setparam("unm", str);

         eprintf("\nPlease enter admin password to access\n%s: ", url);
         read_password(str, sizeof(str));
         eprintf("\n");
         while (str[strlen(str) - 1] == '\r' || str[strlen(str) - 1] == '\n')
            str[strlen(str) - 1] = 0;
         do_crypt(str, pwd);
         setparam("upwd", pwd);
         status = 0;
      }

   } while (status != 200);


   getcfg(lbs->name, "Password file", str, sizeof(str));
   fh = open(str, O_CREAT | O_RDWR | O_BINARY, 0644);
   if (fh < 0) {
      sprintf(error_str, loc("Cannot open file <b>%s</b>"), str);
      strcat(error_str, ": ");
      strcat(error_str, strerror(errno));
      return;
   }

   buf = xmalloc(2 * strlen(p));
   strlcpy(buf, p, 2 * strlen(p));
   adjust_crlf(buf, 2 * strlen(p));

   i = write(fh, buf, strlen(buf));
   if (i < (int) strlen(buf)) {
      sprintf(error_str, loc("Cannot write to <b>%s</b>"), str);
      strcat(error_str, ": ");
      strcat(error_str, strerror(errno));
      close(fh);
      xfree(buf);
      xfree(buffer);
      return;
   }

   TRUNCATE(fh);

   close(fh);

   xfree(buf);
   xfree(buffer);
}

/*------------------------------------------------------------------*/

int save_md5(LOGBOOK * lbs, char *server, MD5_INDEX * md5_index, int n)
{
   char str[256], url[256], file_name[256];
   int i, j;
   FILE *f;

   combine_url(lbs, server, "", url, sizeof(url));
   url_decode(url);
   if (strstr(url, "http://"))
      strcpy(str, url + 7);
   else if (strstr(url, "https://"))
      strcpy(str, url + 8);
   else
      strcpy(str, url);

   for (i = 0; i < (int) strlen(str); i++)
      if (strchr(":/\\ ", str[i]))
         str[i] = '_';

   while (str[strlen(str) - 1] == '_')
      str[strlen(str) - 1] = 0;

   strlcpy(file_name, resource_dir, sizeof(file_name));
   strlcat(file_name, str, sizeof(file_name));
   strlcat(file_name, ".md5", sizeof(file_name));

   f = fopen(file_name, "wt");
   if (f == NULL)
      return -1;

   for (i = 0; i < n; i++) {
      fprintf(f, "ID%d: ", md5_index[i].message_id);
      for (j = 0; j < 16; j++)
         fprintf(f, "%02X", md5_index[i].md5_digest[j]);
      fprintf(f, "\n");
   }

   fclose(f);
   return 1;
}

/*------------------------------------------------------------------*/

int load_md5(LOGBOOK * lbs, char *server, MD5_INDEX ** md5_index)
{
   char str[256], url[256], file_name[256], *p;
   int i, j, x;
   FILE *f;

   *md5_index = NULL;

   combine_url(lbs, server, "", url, sizeof(url));
   url_decode(url);
   if (strstr(url, "http://"))
      strcpy(str, url + 7);
   else if (strstr(url, "https://"))
      strcpy(str, url + 8);
   else
      strcpy(str, url);

   for (i = 0; i < (int) strlen(str); i++)
      if (strchr(":/\\ ", str[i]))
         str[i] = '_';

   while (str[strlen(str) - 1] == '_')
      str[strlen(str) - 1] = 0;

   strlcpy(file_name, resource_dir, sizeof(file_name));
   strlcat(file_name, str, sizeof(file_name));
   strlcat(file_name, ".md5", sizeof(file_name));

   f = fopen(file_name, "rt");
   if (f == NULL)
      return 0;

   for (i = 0; !feof(f); i++) {
      str[0] = 0;
      fgets(str, sizeof(str), f);
      if (!str[0])
         break;

      if (i == 0)
         *md5_index = xcalloc(sizeof(MD5_INDEX), 1);
      else
         *md5_index = xrealloc(*md5_index, sizeof(MD5_INDEX) * (i + 1));

      p = str + 2;

      (*md5_index)[i].message_id = atoi(p);
      while (*p && *p != ' ')
         p++;
      while (*p && *p == ' ')
         p++;

      for (j = 0; j < 16; j++) {
         sscanf(p + j * 2, "%02X", &x);
         (*md5_index)[i].md5_digest[j] = (unsigned char) x;
      }
   }

   fclose(f);
   return i;
}

/*------------------------------------------------------------------*/

BOOL equal_md5(unsigned char m1[16], unsigned char m2[16])
{
   int i;
   for (i = 0; i < 16; i++)
      if (m1[i] != m2[i])
         break;

   return i == 16;
}

/*------------------------------------------------------------------*/

#define SYNC_HTML   1
#define SYNC_CRON   2
#define SYNC_CLONE  3

void mprint(LOGBOOK * lbs, int mode, char *str)
{
   char line[1000];

   if (mode == SYNC_HTML)
      rsprintf("%s\n", str);
   else if (mode == SYNC_CRON) {
      if (_logging_level > 1) {
         sprintf(line, "MIRROR: %s", str);
         write_logfile(lbs, line);
      }
   } else
      eputs(str);
}

void synchronize_logbook(LOGBOOK * lbs, int mode, BOOL sync_all)
{
   int index, i, j, i_msg, i_remote, i_cache, n_remote, n_cache, nserver,
       remote_id, exist_remote, exist_cache, message_id, max_id;
   int all_identical, n_delete;
   char str[2000], url[256], loc_ref[256], rem_ref[256], pwd[256];
   MD5_INDEX *md5_remote, *md5_cache;
   char list[MAX_N_LIST][NAME_LENGTH], error_str[256], *buffer;
   unsigned char digest[16];

   if (!getcfg(lbs->name, "Mirror server", str, sizeof(str))) {
      show_error(loc("No mirror server defined in configuration file"));
      return;
   }

   nserver = strbreak(str, list, MAX_N_LIST, ",");

   for (index = 0; index < nserver; index++) {

      if (mode == SYNC_HTML) {
         rsprintf("<table width=\"100%%\" cellpadding=1 cellspacing=0");

         if (getcfg_topgroup())
            sprintf(loc_ref, "<a href=\"../%s/\">%s</a>", lbs->name_enc, lbs->name);
         else
            sprintf(loc_ref, "<a href=\"%s/\">%s</a>", lbs->name_enc, lbs->name);

         sprintf(str, loc("Synchronizing logbook %s with server \"%s\""), loc_ref, list[index]);
         rsprintf("<tr><td class=\"title1\">%s</td></tr>\n", str);
         rsprintf("</table><p>\n");

         rsprintf("<pre>");
      } else if (mode == SYNC_CLONE) {
         if (list[index][strlen(list[index]) - 1] != '/')
            eprintf("\nRetrieving entries from \"%s/%s\"...\n", list[index], lbs->name);
         else
            eprintf("\nRetrieving entries from \"%s%s\"...\n", list[index], lbs->name);
      }

      /* send partial return buffer */
      flush_return_buffer();

      do {

         n_remote = retrieve_remote_md5(lbs, list[index], &md5_remote, error_str);
         if (n_remote <= 0) {

            if ((n_remote == -2 || n_remote == -3) && mode == SYNC_CLONE) {

               if (n_remote == -3)
                  eprintf("\nInvalid username or password.");

               combine_url(lbs, list[index], "", url, sizeof(url));
               /* ask for username and password */
               eprintf("\nPlease enter username to access\n%s: ", url);
               fgets(str, sizeof(str), stdin);
               while (str[strlen(str) - 1] == '\r' || str[strlen(str) - 1] == '\n')
                  str[strlen(str) - 1] = 0;
               setparam("unm", str);

               eprintf("\nPlease enter password to access\n%s: ", url);
               read_password(str, sizeof(str));
               eprintf("\n");
               while (str[strlen(str) - 1] == '\r' || str[strlen(str) - 1] == '\n')
                  str[strlen(str) - 1] = 0;
               do_crypt(str, pwd);
               setparam("upwd", pwd);

            } else {

               mprint(lbs, mode, error_str);

               if (md5_remote)
                  xfree(md5_remote);

               if (mode == SYNC_HTML)
                  rsprintf("</pre>\n");

               break;
            }
         }

      } while (n_remote <= 0);

      if (n_remote <= 0)
         continue;

      /* load local copy of remote MD5s from file */
      n_cache = load_md5(lbs, list[index], &md5_cache);

      /*---- check for configuration file ----*/

      if (getcfg(lbs->name, "Mirror config", str, sizeof(str)) && atoi(str) == 1
          && md5_cache && mode != SYNC_CLONE) {

         load_config_section(lbs->name, &buffer, error_str);
         if (error_str[0])
            mprint(lbs, mode, error_str);
         else {
            remove_crlf(buffer);
            MD5_checksum(buffer, strlen(buffer), digest);
         }

         /* compare MD5s */
         /*
            eprintf("ID0:    ");
            for (j = 0; j < 16; j++)
            eprintf("%02X", digest[j]);
            eprintf("\nCache : ");
            for (j = 0; j < 16; j++)
            eprintf("%02X", md5_cache[0].md5_digest[j]);
            eprintf("\nRemote: ");
            for (j = 0; j < 16; j++)
            eprintf("%02X", md5_remote[0].md5_digest[j]);
            eprintf("\n\n");
          */

         if (n_remote > 0) {
            /* if config has been changed on this server, but not remotely, send it */
            if (!equal_md5(md5_cache[0].md5_digest, digest)
                && equal_md5(md5_cache[0].md5_digest, md5_remote[0].md5_digest)) {

               if (_logging_level > 1)
                  write_logfile(lbs, "MIRROR send config");

               /* submit configuration section */
               if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                   || atoi(str) == 0) {
                  submit_config(lbs, list[index], buffer, error_str);

                  if (error_str[0])
                     mprint(lbs, mode, error_str);
                  else
                     mprint(lbs, mode, "Local config submitted");
                  md5_cache[0].message_id = -1;
               } else
                  mprint(lbs, mode, "Local config should be submitted");


            } else
               /* if config has been changed remotely, but not on this server, receive it */
               if (!equal_md5(md5_cache[0].md5_digest, md5_remote[0].md5_digest)
                   && equal_md5(md5_cache[0].md5_digest, digest)) {

               if (_logging_level > 1)
                  write_logfile(lbs, "MIRROR receive config");

               if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                   || atoi(str) == 0) {
                  receive_config(lbs, list[index], error_str);

                  if (error_str[0])
                     mprint(lbs, mode, error_str);
                  else
                     mprint(lbs, mode, "Remote config received");

                  md5_cache[0].message_id = -1;
               } else
                  mprint(lbs, mode, loc("Remote config should be received"));

            } else
               /* if config has been changed remotely and on this server, show conflict */
               if (!equal_md5(md5_cache[0].md5_digest, md5_remote[0].md5_digest)
                   && !equal_md5(md5_cache[0].md5_digest, digest)
                   && !equal_md5(md5_remote[0].md5_digest, digest)) {

               if (_logging_level > 1)
                  write_logfile(lbs, "MIRROR config conflict");

               sprintf(str, "%s. ", loc("Configuration has been changed locally and remotely"));
               strcat(str, loc("Please merge manually to resolve conflict"));
               strcat(str, ".");

               mprint(lbs, mode, str);

            } else {
               /* configs are identical */
               md5_cache[0].message_id = -1;
            }
         } else {               /* n_remote == 0 */

            sprintf(str, loc("Logbook \"%s\" does not exist on remote server"), lbs->name);

            mprint(lbs, mode, str);
            continue;

         }

         flush_return_buffer();

         if (buffer)
            xfree(buffer);
      }

      /*---- loop through logbook entries ----*/

      all_identical = TRUE;
      n_delete = 0;

      for (i_msg = 0; i_msg < *lbs->n_el_index; i_msg++) {

         message_id = lbs->el_index[i_msg].message_id;

         /* look for message id in MD5s */
         for (i_remote = 0; i_remote < n_remote; i_remote++)
            if (md5_remote[i_remote].message_id == message_id)
               break;
         exist_remote = i_remote < n_remote;

         for (i_cache = 0; i_cache < n_cache; i_cache++)
            if (md5_cache[i_cache].message_id == message_id)
               break;
         exist_cache = i_cache < n_cache;

         /* if message exists in both lists, compare MD5s */
         if (exist_remote && exist_cache) {

            /* if message has been changed on this server, but not remotely, send it */
            if (!equal_md5(md5_cache[i_cache].md5_digest, lbs->el_index[i_msg].md5_digest)
                && equal_md5(md5_cache[i_cache].md5_digest, md5_remote[i_remote].md5_digest)) {

               all_identical = FALSE;

               if (_logging_level > 1)
                  write_logfile(lbs, "MIRROR send entry #%d", message_id);

               /* submit local message */
               if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                   || atoi(str) == 0) {
                  submit_message(lbs, list[index], message_id, error_str);

                  /* not that submit_message() may have changed attr_list !!! */

                  if (error_str[0])
                     sprintf(str, "%s: %s", loc("Error sending local entry"), error_str);
                  else
                     sprintf(str, "ID%d:\t%s", message_id, loc("Local entry submitted"));
                  mprint(lbs, mode, str);

                  md5_cache[i_cache].message_id = -1;
               } else {
                  sprintf(str, "ID%d:\t%s", message_id, loc("Local entry should be submitted"));
                  mprint(lbs, mode, str);
               }


            } else
               /* if message has been changed remotely, but not on this server, receive it */
               if (!equal_md5(md5_cache[i_cache].md5_digest, md5_remote[i_remote].md5_digest)
                   && equal_md5(md5_cache[i_cache].md5_digest, lbs->el_index[i_msg].md5_digest)) {

               all_identical = FALSE;

               if (mode == SYNC_CLONE) {
                  eprintf("ID%d:\t", message_id);
               } else if (mode == SYNC_HTML) {
                  if (getcfg_topgroup())
                     rsprintf("<a href=\"../%s/%d\">ID%d:</a>\t", lbs->name_enc, message_id, message_id);
                  else
                     rsprintf("<a href=\"%s/%d\">ID%d:</a>\t", lbs->name_enc, message_id, message_id);

                  flush_return_buffer();
               }

               if (_logging_level > 1)
                  write_logfile(lbs, "MIRROR receive entry #%d", message_id);

               if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                   || atoi(str) == 0) {
                  receive_message(lbs, list[index], message_id, error_str, FALSE);

                  if (error_str[0]) {
                     sprintf(str, "%s: %s", loc("Error receiving message"), error_str);
                     mprint(lbs, mode, str);
                  } else if (mode == SYNC_HTML) {

                     rsprintf("%s\n", loc("Remote entry received"));

                  } else if (mode == SYNC_CLONE) {
                     eprintf("%s\n", loc("Remote entry received"));
                  } else {
                     sprintf(str, "ID%d:\t%s", message_id, loc("Remote entry received"));
                     mprint(lbs, mode, str);
                  }

                  md5_cache[i_cache].message_id = -1;

               } else {
                  sprintf(str, "ID%d:\t%s", message_id, loc("Remote entry should be received"));
                  mprint(lbs, mode, str);
               }

            } else
               /* if message has been changed remotely and on this server, show conflict */
               if (!equal_md5(md5_cache[i_cache].md5_digest, md5_remote[i_remote].md5_digest)
                   && !equal_md5(md5_cache[i_cache].md5_digest, lbs->el_index[i_msg].md5_digest)
                   && !equal_md5(md5_remote[i_remote].md5_digest, lbs->el_index[i_msg].md5_digest)) {

               all_identical = FALSE;

               if (mode == SYNC_CLONE) {

                  eprintf
                      ("Warning: Entry #%d has been changed locally and remotely, will not be retrieved\n",
                       message_id);

               } else {

                  if (_logging_level > 1)
                     write_logfile(lbs, "MIRROR conflict entry #%d", message_id);

                  combine_url(lbs, list[index], "", str, sizeof(str));

                  if (getcfg_topgroup())
                     sprintf(loc_ref, "<a href=\"../%s/%d\">%s</a>", lbs->name_enc, message_id, loc("local"));
                  else
                     sprintf(loc_ref, "<a href=\"%d\">%s</a>", message_id, loc("local"));

                  sprintf(rem_ref, "<a href=\"http://%s%d\">%s</a>", str, message_id, loc("remote"));

                  sprintf(str, "ID%d:\t%s. ", message_id, loc("Entry has been changed locally and remotely"));
                  sprintf(str + strlen(str),
                          loc("Please delete %s or %s entry to resolve conflict"), loc_ref, rem_ref);
                  strcat(str, ".");
                  mprint(lbs, mode, str);
               }

            } else {

               /* messages are identical */
               md5_cache[i_cache].message_id = -1;
            }
         }

         if (exist_cache && !exist_remote) {

            /* if message has been changed locally, send it */
            if (!equal_md5(md5_cache[i_cache].md5_digest, lbs->el_index[i_msg].md5_digest)) {

               all_identical = FALSE;

               if (_logging_level > 1)
                  write_logfile(lbs, "MIRROR send entry #%d", message_id);

               /* submit local message */
               if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                   || atoi(str) == 0) {
                  submit_message(lbs, list[index], message_id, error_str);

                  /* not that submit_message() may have changed attr_list !!! */

                  if (error_str[0])
                     sprintf(str, "%s: %s", loc("Error sending local message"), error_str);
                  else
                     sprintf(str, "ID%d:\t%s", message_id, loc("Local entry submitted"));
                  mprint(lbs, mode, str);

                  md5_cache[i_cache].message_id = -1;

               } else {
                  sprintf(str, "ID%d:\t%s", message_id, loc("Local entry should be submitted"));
                  mprint(lbs, mode, str);
               }

            } else {

               /* if message exists only in cache, but not remotely,
                  it must have been deleted remotely, so remove it locally */

               if (!isparam("confirm") && mode == SYNC_HTML) {

                  combine_url(lbs, list[index], "", str, sizeof(str));

                  if (getcfg_topgroup())
                     sprintf(loc_ref, "<a href=\"../%s/%d\">%s</a>", lbs->name_enc, message_id, loc("local"));
                  else
                     sprintf(loc_ref, "<a href=\"%d\">%s</a>", message_id, loc("Local entry"));

                  sprintf(str, loc("%s should be deleted"), loc_ref);
                  rsprintf("ID%d:\t%s\n", message_id, str);
                  n_delete++;

               }
               if (!isparam("confirm") && mode == SYNC_CLONE) {

                  sprintf(str, "ID%d:\t%s", message_id, loc("Entry should be deleted locally"));
                  mprint(lbs, mode, str);

               } else {

                  all_identical = FALSE;

                  if (mode == SYNC_CLONE) {

                     el_delete_message(lbs, message_id, TRUE, NULL, TRUE, TRUE);

                     sprintf(str, "ID%d:\t%s", message_id, loc("Entry deleted locally"));
                     mprint(lbs, mode, str);

                     /* message got deleted from local message list, so redo current index */
                     i_msg--;

                  } else {

                     if (_logging_level > 1)
                        write_logfile(lbs, "MIRROR delete local entry #%d", message_id);

                     if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                         || atoi(str) == 0) {
                        el_delete_message(lbs, message_id, TRUE, NULL, TRUE, TRUE);

                        sprintf(str, "ID%d:\t%s", message_id, loc("Entry deleted locally"));
                        mprint(lbs, mode, str);

                        /* message got deleted from local message list, so redo current index */
                        i_msg--;
                     } else {
                        sprintf(str, "ID%d:\t%s", message_id, loc("Entry should be deleted locally"));
                        mprint(lbs, mode, str);
                     }
                  }

                  /* mark message non-conflicting */
                  md5_cache[i_cache].message_id = -1;
               }
            }
         }

         /* if message does not exist in cache and remotely,
            it must be new, so send it  */
         if (!exist_cache && !exist_remote) {

            all_identical = FALSE;

            if (_logging_level > 1)
               write_logfile(lbs, "MIRROR send entry #%d", message_id);

            remote_id = 0;
            if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                || atoi(str) == 0) {
               remote_id = submit_message(lbs, list[index], message_id, error_str);

               if (error_str[0])
                  sprintf(str, "%s: %s", loc("Error sending local entry"), error_str);
               else if (remote_id != message_id)
                  sprintf(str,
                          "Error: Submitting entry #%d resulted in remote entry #%d\n",
                          message_id, remote_id);
               else
                  sprintf(str, "ID%d:\t%s", message_id, loc("Local entry submitted"));
               mprint(lbs, mode, str);
            } else {
               sprintf(str, "ID%d:\t%s", message_id, loc("Local entry should be submitted"));
               mprint(lbs, mode, str);
            }
         }

         /* if message does not exist in cache but remotely,
            messages were added on both sides, so resubmit local one and retrieve remote one
            if messages are different */
         if (!exist_cache && exist_remote &&
             !equal_md5(md5_remote[i_remote].md5_digest, lbs->el_index[i_msg].md5_digest)) {

            all_identical = FALSE;

            /* find max id both locally and remotely */
            max_id = 1;
            for (i = 0; i < *lbs->n_el_index; i++)
               if (lbs->el_index[i].message_id > max_id)
                  max_id = lbs->el_index[i].message_id;

            for (i = 0; i < n_remote; i++)
               if (md5_remote[i].message_id > max_id)
                  max_id = md5_remote[i].message_id;

            if (_logging_level > 1)
               write_logfile(lbs, "MIRROR change entry #%d to #%d", message_id, max_id + 1);

            /* rearrange local message not to conflict with remote message */
            if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                || atoi(str) == 0) {
               el_move_message(lbs, message_id, max_id + 1);

               sprintf(str, "ID%d:\t", message_id);
               sprintf(str + strlen(str), loc("Changed local entry ID to %d"), max_id + 1);
               mprint(lbs, mode, str);

               /* current message has been changed, so start over */
               i_msg--;
            } else {
               sprintf(str, "ID%d:\t", message_id);
               sprintf(str + strlen(str), loc("Local entry ID should be changed to %d"), max_id + 1);
               mprint(lbs, mode, str);
            }
         }

         flush_return_buffer();
      }

      /* go through remote message which do not exist locally */
      for (i_remote = 0; i_remote < n_remote; i_remote++)
         if (md5_remote[i_remote].message_id) {

            message_id = md5_remote[i_remote].message_id;

            for (i_msg = 0; i_msg < *lbs->n_el_index; i_msg++)
               if (message_id == lbs->el_index[i_msg].message_id)
                  break;

            if (i_msg == *lbs->n_el_index) {

               for (i_cache = 0; i_cache < n_cache; i_cache++)
                  if (md5_cache[i_cache].message_id == message_id)
                     break;
               exist_cache = i_cache < n_cache;

               if (!exist_cache) {

                  all_identical = FALSE;

                  if (mode == SYNC_HTML) {
                     if (getcfg_topgroup())
                        rsprintf("<a href=\"../%s/%d\">ID%d:</a>\t", lbs->name_enc, message_id, message_id);
                     else
                        rsprintf("<a href=\"%s/%d\">ID%d:</a>\t", lbs->name_enc, message_id, message_id);
                     flush_return_buffer();
                  } else if (mode == SYNC_CLONE) {
                     eprintf("ID%d:\t", message_id);
                  }

                  /* if message does not exist locally and in cache, it is new, so retrieve it */
                  if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                      || atoi(str) == 0) {
                     receive_message(lbs, list[index], message_id, error_str, TRUE);

                     if (error_str[0]) {
                        sprintf(str, "Error receiving message: %s", error_str);
                        mprint(lbs, mode, str);
                     } else if (mode == SYNC_HTML) {
                        rsprintf("%s\n", loc("Remote entry received"));
                     } else if (mode == SYNC_CLONE) {
                        eprintf("%s\n", loc("Remote entry received"));
                     } else {
                        sprintf(str, "ID%d:\t%s", message_id, loc("Remote entry received"));
                        mprint(lbs, mode, str);
                     }
                  } else {
                     sprintf(str, "ID%d:\t%s", message_id, loc("Remote entry should be received"));
                     mprint(lbs, mode, str);
                  }

               } else {

                  if (!equal_md5(md5_cache[i_cache].md5_digest, md5_remote[i_remote].md5_digest)) {

                     all_identical = FALSE;

                     /* if message has changed remotely, receive it */
                     if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                         || atoi(str) == 0) {
                        receive_message(lbs, list[index], message_id, error_str, TRUE);

                        if (error_str[0]) {
                           sprintf(str, "Error receiving message: %s", error_str);
                           mprint(lbs, mode, str);
                        } else if (mode == SYNC_HTML) {

                           if (getcfg_topgroup())
                              rsprintf("<a href=\"../%s/%d\">ID%d:</a>\t", lbs->name_enc,
                                       message_id, message_id);
                           else
                              rsprintf("<a href=\"%s/%d\">ID%d:</a>\t", lbs->name_enc,
                                       message_id, message_id);

                           rsprintf("%s\n", loc("Remote entry received"));
                        }
                     } else {
                        sprintf(str, "ID%d:\t%s", message_id, loc("Remote entry should be received"));
                        mprint(lbs, mode, str);
                     }

                  } else {

                     /* if message does not exist locally but in cache, delete remote message */

                     all_identical = FALSE;

                     if (!isparam("confirm") && mode == SYNC_HTML) {

                        combine_url(lbs, list[index], "", str, sizeof(str));
                        sprintf(rem_ref, "<a href=\"http://%s%d\">%s</a>", str,
                                message_id, loc("Remote entry"));

                        sprintf(str, loc("%s should be deleted"), rem_ref);
                        rsprintf("ID%d:\t%s\n", message_id, str);
                        n_delete++;

                     } else if (!isparam("confirm") && mode == SYNC_CLONE) {

                        sprintf(str, "ID%d:\t%s", message_id, loc("Entry should be deleted remotely"));
                        mprint(lbs, mode, str);

                     } else {

                        if (_logging_level > 1)
                           write_logfile(lbs, "MIRROR delete remote entry #%d", message_id);

                        sprintf(str, "%d?cmd=%s&confirm=%s", message_id, loc("Delete"), loc("Yes"));
                        combine_url(lbs, list[index], str, url, sizeof(url));

                        if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str))
                            || atoi(str) == 0) {
                           retrieve_url(url, &buffer, NULL);

                           if (strstr(buffer, "Location: ")) {
                              if (mode == SYNC_HTML)
                                 rsprintf("ID%d:\t%s\n", message_id, loc("Entry deleted remotely"));
                           } else {

                              if (mode == SYNC_HTML && isparam("debug"))
                                 rsputs(buffer);

                              mprint(lbs, mode, loc("Error deleting remote entry"));
                           }

                           md5_cache[i_cache].message_id = -1;
                           xfree(buffer);
                        } else {
                           sprintf(str, "ID%d:\t%s", message_id, loc("Entry should be deleted remotely"));
                           mprint(lbs, mode, str);
                        }
                     }
                  }
               }
            }

            flush_return_buffer();
         }

      xfree(md5_remote);

      /* save remote MD5s in file */
      if (!all_identical) {
         n_remote = retrieve_remote_md5(lbs, list[index], &md5_remote, error_str);
         if (n_remote < 0)
            rsprintf("%s\n", error_str);

         /* keep conflicting messages in cache */
         for (i = 0; i < n_cache; i++)
            if (md5_cache[i].message_id != -1) {

               if (i == 0)
                  memcpy(md5_remote[0].md5_digest, md5_cache[0].md5_digest, 16);
               else
                  for (j = 0; j < n_remote; j++)
                     if (md5_remote[j].message_id == md5_cache[i].message_id) {
                        memcpy(md5_remote[j].md5_digest, md5_cache[i].md5_digest, 16);
                        break;
                     }
            }

         if (!getcfg(lbs->name, "Mirror simulate", str, sizeof(str)) || atoi(str) == 0)
            save_md5(lbs, list[index], md5_remote, n_remote);

         if (md5_remote)
            xfree(md5_remote);
      }

      if (md5_cache)
         xfree(md5_cache);

      if (mode == SYNC_HTML && n_delete) {

         if (getcfg_topgroup())
            rsprintf("<br><b><a href=\"../%s/?cmd=Synchronize&confirm=1\">", lbs->name_enc);
         else {
            if (sync_all)
               rsprintf("<br><b><a href=\"%s/?cmd=Synchronize&confirm=1\">", lbs->name_enc);
            else
               rsprintf("<br><b><a href=\"../%s/?cmd=Synchronize&confirm=1\">", lbs->name_enc);
         }

         if (n_delete > 1)
            rsprintf(loc("Click here to delete %d entries"), n_delete);
         else
            rsprintf(loc("Click here to delete this entry"));

         rsprintf("</a></b>\n");
      }

      if (mode == SYNC_HTML && all_identical)
         rsprintf(loc("All entries identical"));

      if (mode == SYNC_CLONE && all_identical)
         mprint(lbs, mode, loc("All entries identical"));

      if (mode == SYNC_HTML)
         rsprintf("</pre>\n");
   }

   flush_return_buffer();
   keep_alive = 0;
}

/*------------------------------------------------------------------*/

void synchronize(LOGBOOK * lbs, int mode)
{
   int i;
   char str[256], pwd[256];

   if (mode == SYNC_HTML) {
      show_html_header(NULL, FALSE, loc("Synchronization"), TRUE, FALSE, NULL);
      rsprintf("<body>\n");
   }

   if (lbs == NULL) {
      for (i = 0; lb_list[i].name[0]; i++)
         if (getcfg(lb_list[i].name, "mirror server", str, sizeof(str))) {

            if (exist_top_group() && getcfg_topgroup())
               if (lb_list[i].top_group[0]
                   && !strieq(lb_list[i].top_group, getcfg_topgroup()))
                  continue;

            /* skip if excluded */
            if (getcfg(lb_list[i].name, "Mirror exclude", str, sizeof(str)) && atoi(str) == 1)
               continue;

            /* if called by cron, set user name and password */
            if (mode == SYNC_CRON && getcfg(lb_list[i].name, "mirror user", str, sizeof(str))) {
               if (get_user_line(&lb_list[i], str, pwd, NULL, NULL, NULL, NULL) == EL_SUCCESS) {
                  setparam("unm", str);
                  setparam("upwd", pwd);
               }
            }

            synchronize_logbook(&lb_list[i], mode, TRUE);
         }
   } else
      synchronize_logbook(lbs, mode, FALSE);

   if (mode == SYNC_HTML) {
      rsprintf("<table width=\"100%%\" cellpadding=1 cellspacing=0");
      rsprintf("<tr><td class=\"seltitle\"><a href=\".\">%s</a></td></tr>\n", loc("Back"));
      rsprintf("</table><p>\n");

      rsprintf("</body></html>\n");
      flush_return_buffer();
      keep_alive = 0;
   }
}

/*------------------------------------------------------------------*/

void display_line(LOGBOOK * lbs, int message_id, int number, char *mode,
                  int expand, int level, BOOL printable, int n_line,
                  int show_attachments, char *date, char *in_reply_to,
                  char *reply_to, int n_attr_disp,
                  char disp_attr[MAX_N_ATTR + 4][NAME_LENGTH],
                  BOOL disp_attr_link[MAX_N_ATTR + 4],
                  char attrib[MAX_N_ATTR][NAME_LENGTH], int n_attr,
                  char *text, BOOL show_text,
                  char attachment[MAX_ATTACHMENTS][MAX_PATH_LENGTH], char *encoding,
                  BOOL select, int *n_display, char *locked_by, int highlight, regex_t * re_buf)
{
   char str[NAME_LENGTH], ref[256], thumb_name[256], *nowrap, sclass[80], format[256],
       file_name[MAX_PATH_LENGTH], *slist, *svalue;
   char display[NAME_LENGTH], attr_icon[80];
   int i, j, i_line, index, colspan, n_attachments;
   BOOL skip_comma;
   FILE *f;
   struct tm *pts;
   time_t ltime;

   slist = xmalloc((MAX_N_ATTR + 10) * NAME_LENGTH);
   svalue = xmalloc((MAX_N_ATTR + 10) * NAME_LENGTH);

   _current_message_id = message_id;
   compose_base_url(NULL, ref, sizeof(ref));
   sprintf(ref+strlen(ref), "%s/%d", lbs->name_enc, message_id);

   if (strieq(mode, "Summary")) {
      if (number % 2 == 1)
         strcpy(sclass, "list1");
      else
         strcpy(sclass, "list2");
   } else if (strieq(mode, "Full"))
      strcpy(sclass, "list1");
   else if (strieq(mode, "Threaded")) {
      if (highlight) {
         if (highlight == message_id)
            strcpy(sclass, "thread");
         else
            strcpy(sclass, "threadreply");
      } else {
         if (level == 0)
            strcpy(sclass, "thread");
         else
            strcpy(sclass, "threadreply");
      }
   }

   rsprintf("<tr>");

   /* only single cell for threaded display */
   if (strieq(mode, "Threaded")) {
      rsprintf("<td align=left class=\"%s\">", sclass);

      if (locked_by && locked_by[0]) {
         sprintf(str, "%s %s", loc("Entry is currently edited by"), locked_by);
         rsprintf("<img src=\"stop.png\" alt=\"%s\" title=\"%s\">&nbsp;", str, str);
      }

      /* show select box */
      if (select && level == 0)
         rsprintf("<input type=checkbox name=\"s%d\" value=%d>\n", (*n_display)++, message_id);

      for (i = 0; i < level; i++)
         rsprintf("&nbsp;&nbsp;&nbsp;");

      /* display "+" if expandable */
      if (expand == 0 && reply_to[0])
         rsprintf("+&nbsp;");
   }

   nowrap = printable ? "" : "nowrap";
   skip_comma = FALSE;

   evaluate_conditions(lbs, attrib);

   if (strieq(mode, "Threaded")
       && getcfg(lbs->name, "Thread display", display, sizeof(display))) {
      /* check if to use icon from attributes */
      attr_icon[0] = 0;
      if (getcfg(lbs->name, "Thread icon", attr_icon, sizeof(attr_icon))) {
         for (i = 0; i < n_attr; i++)
            if (strieq(attr_list[i], attr_icon))
               break;
         if (i < n_attr && attrib[i][0])
            strcpy(attr_icon, attrib[i]);
         else
            attr_icon[0] = 0;
      }

      if (highlight != message_id)
         rsprintf("\n<a href=\"%s\">", ref);

      if (attr_icon[0])
         rsprintf("\n<img border=0 src=\"icons/%s\" alt=\"%s\" title=\"%s\">\n&nbsp;", attr_icon, attr_icon, attr_icon);
      else {
         /* display standard icons */
         if (level == 0)
            rsprintf("\n<img border=0 src=\"entry.png\" alt=\"%s\" title=\"%s\">\n&nbsp;", loc("Entry"), loc("Entry"));
         else
            rsprintf("\n<img border=0 src=\"reply.png\" alt=\"%s\" title=\"%s\">\n&nbsp;", loc("Reply"), loc("Reply"));
      }
      if (highlight != message_id)
         rsprintf("</a>\n");

      j = build_subst_list(lbs, (char (*)[NAME_LENGTH]) slist, (char (*)[NAME_LENGTH]) svalue, attrib, TRUE);
      sprintf(str, "%d", message_id);
      add_subst_list((char (*)[NAME_LENGTH]) slist, (char (*)[NAME_LENGTH]) svalue, "message id", str, &j);
      add_subst_time(lbs, (char (*)[NAME_LENGTH]) slist, (char (*)[NAME_LENGTH]) svalue,
                     "entry time", date, &j);

      strsubst(display, sizeof(display), (char (*)[NAME_LENGTH]) slist, (char (*)[NAME_LENGTH]) svalue, j);

      if (highlight != message_id)
         rsprintf("\n<a href=\"%s\">\n", ref);
      else
         rsprintf("\n<b>");

      if (is_html(display))
         rsputs(display);
      else
         rsputs2(lbs, display);

      if (highlight != message_id)
         rsprintf("</a>\n", ref);
      else
         rsprintf("</b>\n");
   } else {
      /* show select box */
      if (select && !strieq(mode, "Threaded")) {
         rsprintf("<td class=\"%s\">", sclass);

         if (in_reply_to[0] == 0)
            rsprintf("<input type=checkbox name=\"s%d\" value=%d>\n", (*n_display)++, message_id);
         else
            rsprintf("&nbsp;\n");

         rsprintf("</td>\n");
      }

      if (strieq(mode, "Threaded")) {
         if (highlight != message_id)
            rsprintf("\n<a href=\"%s\">\n", ref);
         else
            rsprintf("\n<b>");
      }

      skip_comma = TRUE;
      for (index = 0; index < n_attr_disp; index++) {
         if (strieq(disp_attr[index], loc("ID"))) {
            if (strieq(mode, "Threaded")) {
               if (level == 0)
                  rsprintf("\n<img border=0 src=\"entry.png\" alt=\"%s\" title=\"%s\">&nbsp;", loc("Entry"), loc("Entry"));
               else
                  rsprintf("\n<img border=0 src=\"reply.png\" alt=\"%s\" title=\"%s\">&nbsp;", loc("Reply"), loc("Reply"));

            } else {
               rsprintf("<td class=\"%s\">", sclass);

               if (locked_by && locked_by[0]) {
                  sprintf(str, "%s %s", loc("Entry is currently edited by"), locked_by);
                  rsprintf("\n<img src=\"stop.png\" alt=\"%s\" title=\"%s\">&nbsp;", str, str);
               }

               if (getcfg(lbs->name, "ID display", display, sizeof(display))) {
                  j = build_subst_list(lbs, (char (*)[NAME_LENGTH]) slist,
                                       (char (*)[NAME_LENGTH]) svalue, attrib, TRUE);
                  sprintf(str, "%d", message_id);
                  add_subst_list((char (*)[NAME_LENGTH]) slist,
                                 (char (*)[NAME_LENGTH]) svalue, "message id", str, &j);
                  add_subst_time(lbs, (char (*)[NAME_LENGTH]) slist,
                                 (char (*)[NAME_LENGTH]) svalue, "entry time", date, &j);

                  strsubst(display, sizeof(display), (char (*)[NAME_LENGTH]) slist,
                           (char (*)[NAME_LENGTH]) svalue, j);

               } else
                  sprintf(display, "%d", message_id);

               rsprintf("\n<a href=\"%s\">&nbsp;&nbsp;%s&nbsp;&nbsp;</a>\n", ref, display);
               rsprintf("</td>\n");
            }
         }

         if (strieq(disp_attr[index], loc("Logbook"))) {
            if (strieq(mode, "Threaded")) {
               if (skip_comma) {
                  rsprintf(" %s", lbs->name);
                  skip_comma = FALSE;
               } else
                  rsprintf(", %s", lbs->name);
            } else {
               if (disp_attr_link == NULL || disp_attr_link[index])
                  rsprintf("\n<td class=\"%s\" %s><a href=\"%s\">%s</a></td>\n", sclass, nowrap, ref,
                           lbs->name);
               else
                  rsprintf("\n<td class=\"%s\" %s>%s</td>\n", sclass, nowrap, lbs->name);
            }
         }

         if (strieq(disp_attr[index], loc("Edit"))) {
            if (!strieq(mode, "Threaded")) {
               rsprintf("\n<td class=\"%s\" %s><a href=\"%s?cmd=%s\">", sclass, nowrap, ref, loc("Edit"));
               rsprintf("\n<img src=\"edit.png\" border=0 alt=\"%s\" title=\"%s\"></a></td>\n", loc("Edit entry"), loc("Edit entry"));
            }
         }

         if (strieq(disp_attr[index], loc("Delete"))) {
            if (!strieq(mode, "Threaded")) {
               rsprintf("\n<td class=\"%s\" %s><a href=\"%s?cmd=%s\">", sclass, nowrap, ref, loc("Delete"));
               rsprintf("\n<img src=\"delete.png\" border=0 alt=\"%s\" title=\"%s\"></a></td>\n", loc("Delete entry"), loc("Delte entry"));
            }
         }

         if (strieq(disp_attr[index], loc("Date"))) {
            if (!getcfg(lbs->name, "Time format", format, sizeof(format)))
               strcpy(format, DEFAULT_TIME_FORMAT);

            ltime = date_to_ltime(date);
            pts = localtime(&ltime);
            my_strftime(str, sizeof(str), format, pts);

            if (strieq(mode, "Threaded")) {
               if (skip_comma) {
                  rsprintf(" %s", str);
                  skip_comma = FALSE;
               } else
                  rsprintf(", %s", str);
            } else {
               if (disp_attr_link == NULL || disp_attr_link[index])
                  rsprintf("\n<td class=\"%s\" %s><a href=\"%s\">%s</a></td>\n", sclass, nowrap, ref, str);
               else
                  rsprintf("\n<td class=\"%s\" %s>%s</td>\n", sclass, nowrap, str);
            }
         }

         for (i = 0; i < n_attr; i++)
            if (strieq(disp_attr[index], attr_list[i])) {
               if (strieq(mode, "Threaded")) {
                  if (strieq(attr_options[i][0], "boolean")) {
                     if (atoi(attrib[i]) == 1) {
                        if (skip_comma) {
                           rsprintf(" ");
                           skip_comma = FALSE;
                        } else
                           rsprintf(", ");

                        if (is_html(attrib[i]))
                           rsputs(attrib[i]);
                        else
                           rsputs2(lbs, attrib[i]);

                        rsprintf("&nbsp");
                     }
                  }

                  else if (attr_flags[i] & AF_ICON) {
                     if (attrib[i][0])
                        rsprintf("&nbsp;\n<img border=0 src=\"icons/%s\" alt=\"%s\" title=\"%s\">&nbsp;", attrib[i],
                                 attrib[i], attrib[i]);
                  }

                  else {
                     if (skip_comma) {
                        rsprintf(" ");
                        skip_comma = FALSE;
                     } else
                        rsprintf(", ");

                     if (is_html(attrib[i]))
                        rsputs(attrib[i]);
                     else
                        rsputs2(lbs, attrib[i]);

                  }
               } else {
                  if (strieq(attr_options[i][0], "boolean")) {
                     if (atoi(attrib[i]) == 1)
                        rsprintf("\n<td class=\"%s\"><input type=checkbox checked disabled></td>\n", sclass);
                     else
                        rsprintf("\n<td class=\"%s\"><input type=checkbox disabled></td>\n", sclass);
                  }

                  else if (attr_flags[i] & AF_DATE) {

                     if (!getcfg(lbs->name, "Date format", format, sizeof(format)))
                        strcpy(format, DEFAULT_DATE_FORMAT);

                     ltime = atoi(attrib[i]);
                     pts = localtime(&ltime);
                     if (ltime == 0)
                        strcpy(str, "-");
                     else
                        my_strftime(str, sizeof(str), format, pts);

                     if (disp_attr_link == NULL || disp_attr_link[index])
                        rsprintf("\n<td class=\"%s\"><a href=\"%s\">%s</a></td>\n", sclass, ref, str);
                     else
                        rsprintf("\n<td class=\"%s\">%s</td>\n", sclass, str);
                  }

                  else if (attr_flags[i] & AF_DATETIME) {

                     if (!getcfg(lbs->name, "Time format", format, sizeof(format)))
                        strcpy(format, DEFAULT_TIME_FORMAT);

                     ltime = atoi(attrib[i]);
                     pts = localtime(&ltime);
                     if (ltime == 0)
                        strcpy(str, "-");
                     else
                        my_strftime(str, sizeof(str), format, pts);

                     if (disp_attr_link == NULL || disp_attr_link[index])
                        rsprintf("\n<td class=\"%s\"><a href=\"%s\">%s</a></td>\n", sclass, ref, str);
                     else
                        rsprintf("\n<td class=\"%s\">%s</td>\n", sclass, str);
                  }

                  else if (attr_flags[i] & AF_ICON) {
                     rsprintf("<td class=\"%s\">", sclass);
                     if (attrib[i][0])
                        rsprintf("\n<img border=0 src=\"icons/%s\" alt=\"%s\" title=\"%s\">", attrib[i], attrib[i], attrib[i]);
                     rsprintf("&nbsp;</td>");
                  }

                  else {
                     rsprintf("<td class=\"%s\">", sclass);

                     if (is_html(attrib[i]))
                        rsputs(attrib[i]);
                     else {
                        if (disp_attr_link == NULL || disp_attr_link[index])
                           rsprintf("<a href=\"%s\">", ref);

                        sprintf(str, "List Change %s", attr_list[i]);
                        if (getcfg(lbs->name, str, display, sizeof(display))) {
                           j = build_subst_list(lbs, (char (*)[NAME_LENGTH]) slist,
                                                (char (*)[NAME_LENGTH]) svalue, attrib, TRUE);
                           sprintf(str, "%d", message_id);
                           add_subst_list((char (*)[NAME_LENGTH]) slist,
                                          (char (*)[NAME_LENGTH]) svalue, "message id", str, &j);
                           add_subst_time(lbs, (char (*)[NAME_LENGTH]) slist,
                                          (char (*)[NAME_LENGTH]) svalue, "entry time", date, &j);

                           strsubst(display, sizeof(display), (char (*)[NAME_LENGTH]) slist,
                                    (char (*)[NAME_LENGTH]) svalue, j);

                        } else
                           strcpy(display, attrib[i]);

                        if (is_html(display))
                           rsputs(display);
                        else {
                           if (*getparam(attr_list[i])) {
                              highlight_searchtext(re_buf + 1 + i, display, str, TRUE);
                              strlcpy(display, str, sizeof(display));
                           } else if (*getparam("subtext") && atoi(getparam("sall"))) {
                              highlight_searchtext(re_buf, display, str, TRUE);
                              strlcpy(display, str, sizeof(display));
                           }
                           rsputs2(lbs, display);
                        }

                        if (disp_attr_link == NULL || disp_attr_link[index])
                           rsprintf("</a>");

                        /* at least one space to produce non-empty table cell */
                        if (!display[0])
                           rsprintf("&nbsp;");
                     }

                     rsprintf("</td>");
                  }
               }
            }
      }

      if (strieq(mode, "Threaded")) {
         if (highlight != message_id)
            rsprintf("</a>\n");
         else
            rsprintf("</b>\n");
      }
   }

   if (strieq(mode, "Threaded") && expand > 1 && show_text) {
      rsprintf("</td></tr>\n");

      rsprintf("<tr><td class=\"summary\">");

      if (expand == 2) {
         for (i = i_line = 0; i < (int) sizeof(str) - 1; i++) {
            str[i] = text[i];
            if (str[i] == '\n')
               i_line++;

            if (i_line == n_line)
               break;
         }
         str[i] = 0;

         /* only show text, not to rip apart HTML documents,
            e.g. only the start of a table */
         strip_html(str);
         if (str[0])
            strencode(str);
         else
            rsputs("&nbsp;");
      } else {
         if (strieq(encoding, "plain")) {
            rsputs("<pre>");
            if (text[0])
               rsputs2(lbs, text);
            else
               rsputs("&nbsp;");
            rsputs("</pre>");
         } else if (strieq(encoding, "ELCode"))
            rsputs_elcode(lbs, text);
         else if (text[0])
            rsputs(text);
         else
            rsputs("&nbsp;");
      }

      rsprintf("</td></tr>\n");
   }

   if (strieq(mode, "Summary") && n_line > 0 && show_text) {
      rsprintf("<td class=\"summary\">");
      for (i = i_line = 0; i < (int) sizeof(str) - 1; i++) {
         str[i] = text[i];
         if (str[i] == '\n')
            i_line++;

         if (i_line == n_line)
            break;
      }
      str[i] = 0;

      /* only show text, not to rip apart HTML documents,
         e.g. only the start of a table */
      strip_html(str);
      if (str[0])
         strencode(str);
      else
         rsputs("&nbsp;");
      rsputs("</td>\n");

      if (strieq(mode, "Threaded"))
         rsprintf("</tr>\n");
   }

   colspan = n_attr_disp;

   if (select)
      colspan++;

   if (strieq(mode, "Full") && show_text) {
      if (!getcfg(lbs->name, "Show text", str, sizeof(str)) || atoi(str) == 1) {
         rsprintf("<tr><td class=\"messagelist\" colspan=%d>", colspan);

         if (strieq(encoding, "plain")) {
            rsputs("<pre>");
            rsputs2(lbs, text);
            rsputs("</pre>");
         } else if (strieq(encoding, "ELCode"))
            rsputs_elcode(lbs, text);
         else
            rsputs(text);

         rsprintf("</td></tr>\n");
      }

      /* count number of attachments */
      n_attachments = 0;
      if (show_attachments) {
         for (index = 0; index < MAX_ATTACHMENTS; index++) {
            if (attachment[index][0]) {
               /* check if attachment is inlined */
               sprintf(str, "[img]elog:/%d[/img]", index + 1);
               if (strieq(encoding, "ELCode") && stristr(text, str))
                  continue;

               n_attachments++;
            }
         }
      }

      if (n_attachments) {
         rsprintf("<tr><td class=\"messagelist\" colspan=%d>", colspan);

         if (attachment[1][0])
            rsprintf("%s: ", loc("Attachments"));
         else
            rsprintf("%s: ", loc("Attachment"));
      }

      for (index = 0; index < MAX_ATTACHMENTS; index++) {
         if (show_attachments && attachment[index][0]) {

            /* check if attachment is inlined */
            sprintf(str, "[img]elog:/%d[/img]", index + 1);
            if (strieq(encoding, "ELCode") && stristr(text, str))
               continue;

            strcpy(str, attachment[index]);
            str[13] = 0;
            sprintf(ref, "../%s/%s/%s", lbs->name, str, attachment[index] + 14);
            url_encode(ref, sizeof(ref));       /* for file names with special characters like "+" */

            strlcpy(thumb_name, lbs->data_dir, sizeof(thumb_name));
            strlcat(thumb_name, attachment[index], sizeof(thumb_name));
            strlcat(thumb_name, ".thumb", sizeof(thumb_name));

            if (!show_attachments) {
               rsprintf("<a href=\"%s\" target=\"_blank\">%s</a>&nbsp;&nbsp;&nbsp;&nbsp; ", ref,
                        attachment[index] + 14);
            } else {

               if (file_exist(thumb_name)) {
                  rsprintf
                      ("<tr><td colspan=%d class=\"attachment\">%s %d: <a href=\"%s\" target=\"_blank\">%s</a>\n",
                       colspan, loc("Attachment"), index + 1, ref, attachment[index] + 14);
                  if (show_attachments) {
                     rsprintf("<tr><td colspan=%d class=\"attachmentframe\">\n", colspan);
                     rsprintf("<a name=\"att%d\" href=\"%s\">\n", index + 1, ref);
                     rsprintf("<img src=\"%s.thumb\" alt=\"%s\" title=\"%s\"></a>\n", ref, attachment[index] + 14, attachment[index] + 14);
                     rsprintf("</td></tr>\n\n");
                  }
               } else {
                  if (is_image(attachment[index])) {
                     rsprintf
                         ("<tr><td colspan=%d class=\"attachment\">%s %d: <a href=\"%s\" target=\"_blank\">%s</a>\n",
                          colspan, loc("Attachment"), index + 1, ref, attachment[index] + 14);
                     if (show_attachments) {
                        rsprintf("</td></tr><tr>");
                        rsprintf("<td colspan=%d class=\"attachmentframe\">", colspan);
                        rsprintf("<img src=\"%s\" alt=\"%s\" title=\"%s\">", ref, attachment[index] + 14, attachment[index] + 14);
                        rsprintf("</td></tr>\n");
                     }
                  } else {
                     rsprintf
                         ("<tr><td colspan=%d class=\"attachment\">%s %d: <a href=\"%s\" target=\"_blank\">%s</a>\n",
                          colspan, loc("Attachment"), index + 1, ref, attachment[index] + 14);

                     strlcpy(file_name, lbs->data_dir, sizeof(file_name));
                     strlcat(file_name, attachment[index], sizeof(file_name));

                     if (is_ascii(file_name) &&
                         !chkext(attachment[index], ".PS") &&
                         !chkext(attachment[index], ".PDF") &&
                         !chkext(attachment[index], ".EPS") &&
                         !chkext(attachment[index], ".HTM") &&
                         !chkext(attachment[index], ".HTML") && show_attachments) {

                        /* display attachment */
                        rsprintf("</td></tr><tr><td colspan=%d class=\"messagelist\"><pre>", colspan);

                        strlcpy(file_name, lbs->data_dir, sizeof(file_name));
                        strlcat(file_name, attachment[index], sizeof(file_name));

                        f = fopen(file_name, "rt");
                        if (f != NULL) {
                           while (!feof(f)) {
                              str[0] = 0;
                              fgets(str, sizeof(str), f);
                              rsputs2(lbs, str);
                           }
                           fclose(f);
                        }

                        rsprintf("</pre>\n");
                     }
                     rsprintf("</td></tr>\n");
                  }
               }
            }
         }
      }

      if (n_attachments)
         rsprintf("</td></tr>\n");
   }

   xfree(slist);
   xfree(svalue);
}

/*------------------------------------------------------------------*/

void display_reply(LOGBOOK * lbs, int message_id, int printable,
                   int expand, int n_line, int n_attr_disp,
                   char disp_attr[MAX_N_ATTR + 4][NAME_LENGTH], BOOL show_text,
                   int level, int highlight, regex_t * re_buf)
{
   char *date, *text, *in_reply_to, *reply_to, *encoding, *locked_by, *attachment, *attrib, *p;
   int status, size;

   text = xmalloc(TEXT_SIZE);
   attachment = xmalloc(MAX_ATTACHMENTS * MAX_PATH_LENGTH);
   attrib = xmalloc(MAX_N_ATTR * NAME_LENGTH);
   date = xmalloc(80);
   in_reply_to = xmalloc(80);
   reply_to = xmalloc(256);
   encoding = xmalloc(80);
   locked_by = xmalloc(256);

   if (locked_by == NULL)
      return;

   reply_to[0] = 0;
   size = TEXT_SIZE;
   status =
       el_retrieve(lbs, message_id, date, attr_list, (void *) attrib,
                   lbs->n_attr, text, &size, in_reply_to, reply_to, (void *) attachment, encoding, locked_by);

   if (status != EL_SUCCESS) {
      xfree(text);
      xfree(attachment);
      xfree(attrib);
      xfree(date);
      xfree(in_reply_to);
      xfree(reply_to);
      xfree(encoding);
      xfree(locked_by);
      return;
   }

   display_line(lbs, message_id, 0, "threaded", expand, level, printable,
                n_line, FALSE, date, in_reply_to, reply_to, n_attr_disp,
                disp_attr, NULL, (void *) attrib, lbs->n_attr, text, show_text,
                NULL, encoding, 0, NULL, locked_by, highlight, &re_buf[0]);

   if (reply_to[0]) {
      p = reply_to;
      do {
         display_reply(lbs, atoi(p), printable, expand, n_line, n_attr_disp,
                       disp_attr, show_text, level + 1, highlight, &re_buf[0]);

         while (*p && isdigit(*p))
            p++;
         while (*p && (*p == ',' || *p == ' '))
            p++;
      } while (*p);
   }

   xfree(text);
   xfree(attachment);
   xfree(attrib);
   xfree(date);
   xfree(in_reply_to);
   xfree(reply_to);
   xfree(encoding);
   xfree(locked_by);
}

/*------------------------------------------------------------------*/

int msg_compare(const void *m1, const void *m2)
{
   return strcoll(((MSG_LIST *) m1)->string, ((MSG_LIST *) m2)->string);
}

int msg_compare_reverse(const void *m1, const void *m2)
{
   return strcoll(((MSG_LIST *) m2)->string, ((MSG_LIST *) m1)->string);
}

/*------------------------------------------------------------------*/

char *param_in_str(char *str, char *param)
{
   char *p;

   p = str;
   do {
      if (stristr(p, param) == NULL)
         return NULL;

      p = stristr(p, param);

      /* if parameter is value of another parameter, skip it */
      if (p > str + 1 && *(p - 1) == '=')
         p += strlen(param);
      else
         return p;

      if (*p == 0)
         return NULL;

   } while (1);
}

/*------------------------------------------------------------------*/

void subst_param(char *str, int size, char *param, char *value)
{
   int len;
   char *p1, *p2, *s;

   if (!value[0]) {
      /* remove parameter */
      s = param_in_str(str, param);

      if (s == NULL)
         return;

      /* remove parameter */
      p1 = s - 1;

      for (p2 = p1 + strlen(param) + 1; *p2 && *p2 != '&'; p2++);
      strlcpy(p1, p2, size - ((int) p1 - (int) str));

      if (!strchr(str, '?') && strchr(str, '&'))
         *strchr(str, '&') = '?';

      return;
   }

   if ((p1 = param_in_str(str, param)) == NULL) {
      if (strchr(str, '?'))
         strlcat(str, "&", size);
      else
         strlcat(str, "?", size);

      strlcat(str, param, size);
      strlcat(str, "=", size);
      strlcat(str, value, size);
      return;
   }

   p1 += strlen(param) + 1;
   for (p2 = p1; *p2 && *p2 != '&'; p2++);
   len = (int) p2 - (int) p1;
   if (len > (int) strlen(value)) {
      /* new value is shorter than old one */
      strlcpy(p1, value, size - ((int) p1 - (int) str));
      strlcpy(p1 + strlen(value), p2, size - ((int) p1 + strlen(value) - (int) str));
   } else {
      /* new value is longer than old one */
      s = xmalloc(size);
      strlcpy(s, p2, size);
      strlcpy(p1, value, size - ((int) p1 - (int) str));
      strlcat(p1, s, size - ((int) p1 + strlen(value) - (int) str));
      xfree(s);
   }

}

/*------------------------------------------------------------------*/

BOOL is_user_allowed(LOGBOOK * lbs, char *command)
{
   char str[1000], users[2000];
   char list[MAX_N_LIST][NAME_LENGTH];
   int i, n;

   /* check for user level access */
   if (!getcfg(lbs->name, "Password file", str, sizeof(str)))
      return TRUE;

   /* check for deny */
   sprintf(str, "Deny %s", command);
   if (getcfg(lbs->name, str, users, sizeof(users))) {
      /* check if current user in list */
      n = strbreak(users, list, MAX_N_LIST, ",");
      for (i = 0; i < n; i++)
         if (strieq(list[i], getparam("unm")))
            break;

      if (i < n)
         return FALSE;
   }

   /* check admin command */
   if (strieq(command, loc("Admin"))) {
      if (getcfg(lbs->name, "Admin user", str, sizeof(str))) {
         return is_admin_user(lbs->name, getparam("unm"));
      }
   }

   /* check for allow */
   sprintf(str, "Allow %s", command);
   if (!getcfg(lbs->name, str, users, sizeof(users)))
      return TRUE;

   /* check if current user in list */
   n = strbreak(users, list, MAX_N_LIST, ",");
   for (i = 0; i < n; i++)
      if (strieq(list[i], getparam("unm")))
         return TRUE;

   return FALSE;
}

/*------------------------------------------------------------------*/

BOOL is_command_allowed(LOGBOOK * lbs, char *command)
{
   char str[1000], menu_str[1000], other_str[1000];
   char menu_item[MAX_N_LIST][NAME_LENGTH];
   int i, n;

   if (command[0] == 0)
      return TRUE;

   /* check for guest access */
   if (!getcfg(lbs->name, "Guest Menu commands", menu_str, sizeof(menu_str))
       || *getparam("unm") != 0)
      getcfg(lbs->name, "Menu commands", menu_str, sizeof(menu_str));

   /* default menu commands */
   if (menu_str[0] == 0) {
      strcpy(menu_str, "List, New, Edit, Delete, Reply, Duplicate, Find, ");

      if (getcfg(lbs->name, "Password file", str, sizeof(str))) {

         if (is_admin_user(lbs->name, getparam("unm"))) {

            strcat(menu_str, "Admin, ");
            strcat(menu_str, "Change config file, ");
            strcat(menu_str, "Delete this logbook, ");
            strcat(menu_str, "Rename this logbook, ");
            strcat(menu_str, "Create new logbook, ");
            strcat(menu_str, "GetPwdFile, ");


            if (getcfg(lbs->name, "Mirror server", str, sizeof(str)))
               strcat(menu_str, "Synchronize, ");

            if (is_admin_user("global", getparam("unm"))) {

               if (lbs->top_group[0]) {
                  sprintf(str, "Change [global %s]", lbs->top_group);
                  strcat(menu_str, str);
                  strcat(menu_str, ", ");
               }

               if (!lbs->top_group[0] || (is_admin_user("global", getparam("unm")))) {

                  strcat(menu_str, "Change [global]");
                  strcat(menu_str, ", ");
               }
            }
         }
         strcat(menu_str, "Config, Logout, ");
      } else {
         if (getcfg(lbs->name, "Mirror server", str, sizeof(str)))
            strcat(menu_str, "Synchronize, ");
         strcat(menu_str, "Config, ");
         strcat(menu_str, "Change [global], ");
         strcat(menu_str, "Delete this logbook, ");
         strcat(menu_str, "Rename this logbook, ");
         strcat(menu_str, "Create new logbook, ");
      }

      strcat(menu_str, "Help, HelpELCode, ");

   } else {
      /* check for admin command */
      n = strbreak(menu_str, menu_item, MAX_N_LIST, ",");
      menu_str[0] = 0;
      for (i = 0; i < n; i++) {
         if (strcmp(menu_item[i], "Admin") == 0) {
            if (!is_admin_user(lbs->name, getparam("unm")))
               continue;
         }
         strcat(menu_str, menu_item[i]);
         strcat(menu_str, ", ");
      }

      if (is_admin_user(lbs->name, getparam("unm"))) {

         if (getcfg(lbs->name, "Mirror server", str, sizeof(str)))
            strcat(menu_str, "Synchronize, ");
         strcat(menu_str, "Change config file, ");
         strcat(menu_str, "Delete this logbook, ");
         strcat(menu_str, "Rename this logbook, ");
         strcat(menu_str, "Create new logbook, ");
         strcat(menu_str, "GetPwdFile, ");

         if (is_admin_user("global", getparam("unm"))) {

            if (lbs->top_group[0]) {
               sprintf(str, "Change [global %s]", lbs->top_group);
               strcat(menu_str, str);
               strcat(menu_str, ", ");
            }

            if (!lbs->top_group[0] || (is_admin_user("global", getparam("unm")))) {

               strcat(menu_str, "Change [global]");
               strcat(menu_str, ", ");
            }
         }
      }
   }

   /* check list menu commands */
   str[0] = 0;
   if (!getcfg(lbs->name, "Guest List Menu commands", str, sizeof(str))
       || *getparam("unm") != 0)
      getcfg(lbs->name, "list menu commands", str, sizeof(str));

   if (!str[0]) {
      if (!getcfg(lbs->name, "Guest Find Menu commands", str, sizeof(str))
          || *getparam("unm") != 0)
         getcfg(lbs->name, "Find Menu commands", str, sizeof(str));
   }

   if (str[0])
      strlcat(menu_str, str, sizeof(menu_str));
   else {
      strlcat(menu_str, "New, Find, Select, Last x, Help, HelpELCode, ", sizeof(menu_str));

      if (getcfg(lbs->name, "Password file", str, sizeof(str)))
         strlcat(menu_str, "Admin, Config, Logout, ", sizeof(menu_str));
      else
         strlcat(menu_str, "Config, ", sizeof(menu_str));
   }

   strcpy(other_str, "Update, Upload, Submit, Preview, Back, Search, Save, Download, CSV Import, ");
   strcat(other_str, "Cancel, First, Last, Previous, Next, Requested, Forgot, ");

   /* admin commands */
   if (is_admin_user(lbs->name, getparam("unm"))) {
      strcat(other_str, "Remove user, New user, Activate, ");
   } else if (getcfg(lbs->name, "Self register", str, sizeof(str)) && atoi(str) > 0) {
      strcat(other_str, "Remove user, New user, ");
   }

   /* allow import if CSV import is present */
   if (strstr(menu_str, loc("CSV Import")))
      strcat(other_str, "Import, ");

   /* allow change password if "config" possible */
   if (strieq(command, loc("Change password")) && strstr(menu_str, "Config")) {
      return TRUE;
   }
   /* exclude non-localized submit for elog */
   else if (command[0] && strieq(command, "Submit")) {
      return TRUE;
   }
   /* exclude other non-localized commands */
   else if (command[0] && strieq(command, "GetMD5")) {
      return TRUE;
   }
   /* check if command is present in the menu list */
   else if (command[0]) {
      n = strbreak(menu_str, menu_item, MAX_N_LIST, ",");
      for (i = 0; i < n; i++)
         if (strieq(command, menu_item[i]) || strieq(command, loc(menu_item[i])))
            break;

      if (i == n) {
         n = strbreak(other_str, menu_item, MAX_N_LIST, ",");
         for (i = 0; i < n; i++)
            if (strieq(command, loc(menu_item[i])))
               break;

         if (i == n)
            return FALSE;
      }
   }

   return TRUE;
}

/*------------------------------------------------------------------*/

void build_ref(char *ref, int size, char *mode, char *expand, char *new_entries)
{
   char str[1000];

   if (strchr(getparam("cmdline"), '?'))
      strlcat(ref, strchr(getparam("cmdline"), '?'), size);

   /* eliminate old search */
   if (strstr(ref, "cmd=Search&"))
      strcpy(strstr(ref, "cmd=Search&"), strstr(ref, "cmd=Search&") + 11);

   /* eliminate old mode if new one is present */
   if (mode[0])
      subst_param(ref, size, "mode", mode);

   /* eliminate old expand if new one is present */
   if (expand[0])
      subst_param(ref, size, "expand", expand);

   /* eliminate old new_entries if new one is present */
   if (new_entries[0])
      subst_param(ref, size, "new_entries", new_entries);

   /* eliminate old last= */
   subst_param(ref, size, "last", getparam("last"));

   /* replace any '&' by '&amp;' */
   strlcpy(str, ref, sizeof(str));
   strencode2(ref, str);
}

/*------------------------------------------------------------------*/

void show_page_filters(LOGBOOK * lbs, int n_msg, int page_n, BOOL mode_commands, BOOL threaded)
{
   int cur_exp, n, i, j, i1, i2, index, size;
   char ref[256], str[NAME_LENGTH], comment[NAME_LENGTH], list[MAX_N_LIST][NAME_LENGTH], option[NAME_LENGTH];

   rsprintf("<tr><td class=\"menuframe\">\n");
   rsprintf("<table width=\"100%%\" border=0 cellpadding=0 cellspacing=0>\n");

   rsprintf("<tr>\n");

   if (mode_commands) {
      rsprintf("<td class=\"menu2a\">\n");

      if (!getcfg(lbs->name, "Show text", str, sizeof(str)) || atoi(str) == 1) {
         if (page_n != 1)
            sprintf(ref, "page%d", page_n);
         else
            ref[0] = 0;
         build_ref(ref, sizeof(ref), "full", "", "");
         rsprintf("&nbsp;<a href=\"%s\">%s</a>&nbsp;|", ref, loc("Full"));
      }

      if (page_n != 1)
         sprintf(ref, "page%d", page_n);
      else
         ref[0] = 0;
      build_ref(ref, sizeof(ref), "summary", "", "");
      rsprintf("&nbsp;<a href=\"%s\">%s</a>&nbsp;|", ref, loc("Summary"));

      if (page_n != 1)
         sprintf(ref, "page%d", page_n);
      else
         ref[0] = 0;
      build_ref(ref, sizeof(ref), "threaded", "", "");
      rsprintf("&nbsp;<a href=\"%s\">%s</a>&nbsp;", ref, loc("Threaded"));

      if (threaded) {
         if (page_n != 1)
            sprintf(ref, "page%d", page_n);
         else
            ref[0] = 0;

         cur_exp = 1;
         if (getcfg(lbs->name, "Expand default", str, sizeof(str)))
            cur_exp = atoi(str);
         if (isparam("expand"))
            cur_exp = atoi(getparam("expand"));

         if (cur_exp > 0) {
            sprintf(str, "%d", cur_exp > 0 ? cur_exp - 1 : 0);
            build_ref(ref, sizeof(ref), "", str, "");
            rsprintf("|&nbsp;<a href=\"%s\">%s</a>&nbsp;", ref, loc("Collapse"));
         } else
            rsprintf("|&nbsp;%s&nbsp;", loc("Collapse"));

         if (cur_exp < 3) {
            if (page_n != 1)
               sprintf(ref, "page%d", page_n);
            else
               ref[0] = 0;
            sprintf(str, "%d", cur_exp < 3 ? cur_exp + 1 : 3);
            build_ref(ref, sizeof(ref), "", str, "");
            rsprintf("|&nbsp;<a href=\"%s\">%s</a>&nbsp;", ref, loc("Expand"));
         } else
            rsprintf("|&nbsp;%s&nbsp;", loc("Expand"));
      }

      rsprintf("</td>\n");
   }

   rsprintf("<td class=\"menu2b\">\n");

   /*---- filter menu text ----*/

   if (getcfg(lbs->name, "filter menu text", str, sizeof(str))) {
      FILE *f;
      char file_name[256], *buf;

      /* check if file starts with an absolute directory */
      if (str[0] == DIR_SEPARATOR || str[1] == ':')
         strcpy(file_name, str);
      else {
         strlcpy(file_name, resource_dir, sizeof(file_name));
         strlcat(file_name, str, sizeof(file_name));
      }

      f = fopen(file_name, "rb");
      if (f != NULL) {
         fseek(f, 0, SEEK_END);
         size = TELL(fileno(f));
         fseek(f, 0, SEEK_SET);

         buf = xmalloc(size + 1);
         fread(buf, 1, size, f);
         buf[size] = 0;
         fclose(f);

         rsputs(buf);

      } else
         rsprintf("<center><b>Error: file <i>\"%s\"</i> not found</b></center>", file_name);
   }

   ref[0] = 0;
   if (!isparam("new_entries") || atoi(getparam("new_entries")) == 0) {
      build_ref(ref, sizeof(ref), "", "", "1");
      rsprintf("<a href=\"%s\"><img align=\"middle\" border=\"0\" src=\"new_entry.png\" alt=\"%s\" title=\"%s\"></a>&nbsp;&nbsp;", 
         ref, loc("Show only new entries"), loc("Show only new entries"));
   } else {
      build_ref(ref, sizeof(ref), "", "", "0");
      rsprintf("<a href=\"%s\"><img align=\"middle\" border=\"0\" src=\"all_entry.png\" alt=\"%s\" title=\"%s\"></a>&nbsp;&nbsp;", 
         ref, loc("Show all entries"), loc("Show all entries"));
   }

   if (getcfg(lbs->name, "Quick filter", str, sizeof(str))) {

      n = strbreak(str, list, MAX_N_LIST, ",");

      if (getcfg(lbs->name, "Case sensitive search", str, sizeof(str)) && atoi(str))
         rsprintf("<input type=hidden name=\"casesensitive\" value=1>\n");

      for (index = 0; index < n; index++) {
         if (strieq(list[index], loc("Date"))) {
            i = atoi(getparam("last"));

            rsprintf("<input type=submit value=\"%s\">&nbsp;\n", loc("Show last"));

            rsprintf("<select name=last onChange=\"document.form1.submit()\">\n");

            rsprintf("<option value=\"_all_\">%s\n", loc("All entries"));

            rsprintf("<option %s value=1>%s\n", i == 1 ? "selected" : "", loc("Day"));
            rsprintf("<option %s value=7>%s\n", i == 7 ? "selected" : "", loc("Week"));
            rsprintf("<option %s value=31>%s\n", i == 31 ? "selected" : "", loc("Month"));
            rsprintf("<option %s value=92>%s\n", i == 92 ? "selected" : "", loc("3 Months"));
            rsprintf("<option %s value=182>%s\n", i == 182 ? "selected" : "", loc("6 Months"));
            rsprintf("<option %s value=364>%s\n", i == 364 ? "selected" : "", loc("Year"));

            rsprintf("</select>\n");
         } else {
            rsprintf("<input type=submit value=\"%s:\">&nbsp;\n", list[index]);

            for (i = 0; i < MAX_N_ATTR; i++)
               if (strieq(attr_list[i], list[index]))
                  break;

            if (attr_options[i][0][0] == 0) {

               if (attr_flags[i] & (AF_DATE | AF_DATETIME)) {

                  rsprintf("<select name=\"%s\" onChange=\"document.form1.submit()\">\n", list[index]);

                  i = atoi(getparam(list[index]));

                  rsprintf("<option %s value=-364>%s %s\n", i == -364 ? "selected" : "",
                           loc("Last"), loc("Year"));
                  rsprintf("<option %s value=-182>%s %s\n", i == -182 ? "selected" : "",
                           loc("Last"), loc("6 Months"));
                  rsprintf("<option %s value=-92>%s %s\n", i == -92 ? "selected" : "",
                           loc("Last"), loc("3 Months"));
                  rsprintf("<option %s value=-31>%s %s\n", i == -31 ? "selected" : "",
                           loc("Last"), loc("Month"));
                  rsprintf("<option %s value=-7>%s %s\n", i == -7 ? "selected" : "",
                           loc("Last"), loc("Week"));
                  rsprintf("<option %s value=-1>%s %s\n", i == -1 ? "selected" : "", loc("Last"), loc("Day"));

                  rsprintf("<option %s value=\"_all_\">%s\n", i == 0 ? "selected" : "", loc("All entries"));

                  rsprintf("<option %s value=1>%s %s\n", i == 1 ? "selected" : "", loc("Next"), loc("Day"));
                  rsprintf("<option %s value=7>%s %s\n", i == 7 ? "selected" : "", loc("Next"), loc("Week"));
                  rsprintf("<option %s value=31>%s %s\n", i == 31 ? "selected" : "",
                           loc("Next"), loc("Month"));
                  rsprintf("<option %s value=92>%s %s\n", i == 92 ? "selected" : "",
                           loc("Next"), loc("3 Months"));
                  rsprintf("<option %s value=182>%s %s\n", i == 182 ? "selected" : "",
                           loc("Next"), loc("6 Months"));
                  rsprintf("<option %s value=364>%s %s\n", i == 364 ? "selected" : "",
                           loc("Next"), loc("Year"));


                  rsprintf("</select>\n");
               }

               else {
                  rsprintf("<input type=text onChange=\"document.form1.submit()\"");
                  rsprintf(" name=\"%s\" value=\"%s\">\n", list[index], getparam(list[index]));
               }
            } else {
               rsprintf("<select name=\"%s\" onChange=\"document.form1.submit()\">\n", list[index]);

               rsprintf("<option value=\"_all_\">%s\n", loc("All entries"));

               if (i < MAX_N_ATTR) {
                  for (j = 0; j < MAX_N_LIST && attr_options[i][j][0]; j++) {
                     comment[0] = 0;
                     if (attr_flags[i] & AF_ICON) {
                        sprintf(str,