summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--LICENSE340
-rw-r--r--VERSION1
-rw-r--r--doc/Changelog31
-rw-r--r--doc/exilog.txt171
-rw-r--r--doc/mysql-db-script.sql199
-rw-r--r--doc/pgsql-db-script.sql624
-rw-r--r--exilog.conf-example143
-rwxr-xr-xexilog_agent.pl471
-rwxr-xr-xexilog_cgi.pl124
-rw-r--r--exilog_cgi_html.pm984
-rw-r--r--exilog_cgi_messages.pm816
-rw-r--r--exilog_cgi_param.pm74
-rw-r--r--exilog_cgi_queues.pm131
-rw-r--r--exilog_cgi_servers.pm102
-rwxr-xr-xexilog_cleanup.pl65
-rw-r--r--exilog_config.pm63
-rw-r--r--exilog_jscript.js1232
-rw-r--r--exilog_parse.pm351
-rw-r--r--exilog_sql.pm556
-rw-r--r--exilog_stylesheet.css233
-rw-r--r--exilog_util.pm136
-rw-r--r--icons/address.pngbin0 -> 630 bytes
-rw-r--r--icons/arrival.pngbin0 -> 935 bytes
-rw-r--r--icons/arrival_auth.pngbin0 -> 2246 bytes
-rw-r--r--icons/arrival_local.pngbin0 -> 2240 bytes
-rw-r--r--icons/arrival_normal.pngbin0 -> 1801 bytes
-rw-r--r--icons/arrival_tls.pngbin0 -> 2172 bytes
-rw-r--r--icons/arrival_tls_auth.pngbin0 -> 2600 bytes
-rw-r--r--icons/deferral_normal.pngbin0 -> 1977 bytes
-rw-r--r--icons/deferral_tls.pngbin0 -> 2354 bytes
-rw-r--r--icons/deferred.pngbin0 -> 924 bytes
-rw-r--r--icons/delivered.pngbin0 -> 481 bytes
-rw-r--r--icons/delivery.pngbin0 -> 938 bytes
-rw-r--r--icons/delivery_normal.pngbin0 -> 1842 bytes
-rw-r--r--icons/delivery_tls.pngbin0 -> 2235 bytes
-rw-r--r--icons/dns.pngbin0 -> 991 bytes
-rw-r--r--icons/dsn_warning.pngbin0 -> 760 bytes
-rw-r--r--icons/errmsg.pngbin0 -> 775 bytes
-rw-r--r--icons/error.pngbin0 -> 957 bytes
-rw-r--r--icons/error_normal.pngbin0 -> 1772 bytes
-rw-r--r--icons/error_tls.pngbin0 -> 2176 bytes
-rw-r--r--icons/event_type.pngbin0 -> 816 bytes
-rw-r--r--icons/find.pngbin0 -> 688 bytes
-rw-r--r--icons/frozen.pngbin0 -> 775 bytes
-rw-r--r--icons/helo.pngbin0 -> 883 bytes
-rw-r--r--icons/ident.pngbin0 -> 712 bytes
-rw-r--r--icons/queue_deferred.pngbin0 -> 2093 bytes
-rw-r--r--icons/queue_frozen.pngbin0 -> 2031 bytes
-rw-r--r--icons/queue_normal.pngbin0 -> 1562 bytes
-rw-r--r--icons/queued.pngbin0 -> 710 bytes
-rw-r--r--icons/reject_postdata.pngbin0 -> 1996 bytes
-rw-r--r--icons/reject_predata.pngbin0 -> 2086 bytes
-rw-r--r--icons/router_transport.pngbin0 -> 1041 bytes
-rw-r--r--icons/server.pngbin0 -> 790 bytes
-rw-r--r--icons/server_normal.pngbin0 -> 1791 bytes
-rw-r--r--icons/size.pngbin0 -> 765 bytes
-rw-r--r--icons/stats_h24.pngbin0 -> 2706 bytes
-rw-r--r--icons/stopwatch.pngbin0 -> 1004 bytes
-rw-r--r--icons/timerange.pngbin0 -> 1007 bytes
-rw-r--r--icons/unknown.pngbin0 -> 717 bytes
60 files changed, 6847 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3912109
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..2eb3c4f
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.5
diff --git a/doc/Changelog b/doc/Changelog
new file mode 100644
index 0000000..f3d3c11
--- /dev/null
+++ b/doc/Changelog
@@ -0,0 +1,31 @@
+0.5 - Feature: Add support for basic user access
+ levels.
+ - Feature: Add support to deliver,cancel and
+ delete queued messages.
+ - Make "pretty process names" for the agent
+ configurable (Thanks to Sven Hartge).
+
+0.4 - Fix network matching code
+ - Add pidfile support for the agent.
+ (Thanks to Sven Hartge)
+ - Add support for "log_timezone = true"
+ (Thanks to Jeremy Harris)
+
+0.3 - Fix detection of post-DATA rejects
+ (Thanks to Chris Lear)
+ - Fix quoting of "completed" timestamp
+ in exilog_sql.pm.
+ - Fix delivery parsing when +sender_on_delivery
+ is set.
+ - Docs: mention exilog_cleanup.pl.
+ - Docs: mention possibility to run the
+ agent non-root.
+
+0.2 - Use FindBin to find out binary location
+ - Fix bug in SQL quoting.
+ - Remove link from "Server" to the unfinished
+ Queue display :)
+ - Docs: Mention Net::Netmask prerequisite.
+ - Docs: Add small usage section.
+
+0.01 Initial Release
diff --git a/doc/exilog.txt b/doc/exilog.txt
new file mode 100644
index 0000000..da0bcd2
--- /dev/null
+++ b/doc/exilog.txt
@@ -0,0 +1,171 @@
+Exilog - Central logging and reporting tool for Exim
+----------------------------------------------------
+Author: Tom Kistner <tom@duncanthrax.net>
+
+
+Introduction
+------------
+Exilog is a tool to centralize and visualize Exim logs
+across multiple Exim servers. It is used in addition to
+Exim's standard or syslog logging. It does not require
+changing Exim or its logging style (In fact you don't
+even need to restart your Exim(s) to install Exilog).
+
+Exilog is SQL-based and requires
+
+- A SQL Server (mysql and postgres are supported)
+- An HTTP Server with CGI support (Apache comes to mind)
+- Perl with
+ o DBD/DBI SQL Database modules for the selected database.
+ o Net::Netmask module
+ You can get these modules via CPAN, but there is a good
+ chance that your OS distribution has precompiled packages
+ available.
+- A modern browser (recent Mozilla, Firefox, IE5/6, Safari)
+
+
+Target Audience
+---------------
+Postmasters who want to be able to troubleshoot email
+delivery across their Exim installations, no matter if
+used as relays or backend IMAP and POP toasters.
+
+Postmasters who want to offload support grungework to
+staff who is less proficient with grep, sed and awk.
+
+
+Features
+--------
+Search for addresses, hosts (names and IP addresses),
+messages IDs and ident strings.
+
+Filter by event types: Arrivals, Deliveries, Deferrals,
+Errors, Rejects and messages that are still on-queue.
+
+Message actions: Force delivery, cancel and delete.
+
+Filter by time range, servers and server groups.
+
+See basic host statistics, message sizes, message transfer
+times.
+
+Point-and-click on message IDs, IP addresses, hostnames to
+get different filtering results.
+
+Track messages across servers by header message ID.
+
+
+Installation
+------------
+An Exilog installation consist of four parts:
+
+1) The database holding the log information.
+2) The web interface.
+3) The agents on the Exim servers.
+4) Database cleanup (via Cron)
+
+These parts can reside on different machines, or all be
+on the same machine. For best results, the database and
+web interface should be on the same physical box, however.
+
+1) Installing the database.
+
+ Select if you want to use MySQL or Postgres. MySQL is
+ somehow preferred since its default case insensitivy
+ is better suited for the job.
+
+ Create a database using the respective SQL scripts from
+ /doc. For postgres, you might have to slightly edit the
+ script to change the 'exilog' user name (or create the
+ 'exilog' user first).
+
+ If necessary, create a database user that has
+ full rights on the new database.
+
+ Make sure the database is reachable by TCP/IP from each
+ of your Exim servers.
+
+
+2) Installing the Web Interface.
+
+ Untar the exilog distribution somewhere where your HTTP
+ server can reach it (/var/www/localhost/htdocs/exilog ...
+ you get the idea).
+
+ Rename the exilog.conf-example file to exilog.conf and
+ edit it. It is fully commented. Then return to this document.
+
+ exilog_cgi.pl is the web interface. Set it up as
+ DirectoryIndex if you like.
+
+ Optionally, set up access controls. You should also deny
+ read access to exilog.conf from HTTP clients.
+
+ Open your browser and open exilog_cgi.pl. If you see
+ the "Messages" tab you are fine.
+
+ If you want to restrict access to the web interface, set
+ up basic authentication (possibly via .htaccess/.htpasswd).
+
+ Now we need to feed some data into the database.
+
+
+3) Installing the Exim server agent(s).
+
+ You'll need to deploy one Exilog agent on each exim server
+ you run.
+
+ For each server, untar the Exilog distribution somewhere,
+ overwrite the vanilla exilog.conf with the one you edited
+ in step 2, then open it and tweak the "agent" section to
+ match the server you are installing it on. Also tweak the
+ SQL section to include host and port definitions of your SQL
+ server so the agent knows where to connect to.
+
+ Then run exilog_agent.pl as root. You might want to include
+ a start/stop procedure for the agent in your Exim rc file.
+
+ You can also run the agent as a non-root user if that UID
+ * Can read Exim's logs.
+ * Write the configured agent log file.
+ * Is a trusted user in Exim.
+ Of course root can do all that without further configuration
+ tweaks. Using Exim's own effective UID is also possible.
+
+ Sending SIGTERM to the agent parent process will make it
+ cleanly quit, including all of its children.
+
+ When the agent is started, it will pump the current log file
+ into the database (this can take a while), then tail it. It
+ will automatically detect log rotation and re-open the file
+ if necessary.
+
+
+4) Setting up the database cleanup script
+
+ Set up exilog_cleanup.pl to run daily via cron. This will
+ typically reside on the database or web host. Remember to
+ set the "cleanup->cutoff" parameter in exilog.conf to the
+ number of days worth of data you want to keep in the database.
+
+
+
+Usage
+-----
+That should be pretty straightforward. One detail is important:
+
+When searching for addresses and hostnames, you must use SQL
+wildcards when only specifying a substring:
+
+% Matches any string of zero or more characters.
+_ Matches any one character.
+
+Example: You want to find all mails with addresses that contain
+'joe', so you'd search for '%joe%'.
+
+
+--
+Tom Kistner
+<tom@duncanthrax.net>
+June 2005
+
diff --git a/doc/mysql-db-script.sql b/doc/mysql-db-script.sql
new file mode 100644
index 0000000..7ed82a6
--- /dev/null
+++ b/doc/mysql-db-script.sql
@@ -0,0 +1,199 @@
+# phpMyAdmin MySQL-Dump
+# version 2.3.2
+# http://www.phpmyadmin.net/ (download page)
+#
+# Host: localhost
+# Erstellungszeit: 02. Juni 2005 um 15:40
+# Server Version: 3.23.47
+# PHP-Version: 4.1.2
+# Datenbank: `exilog`
+# --------------------------------------------------------
+
+#
+# Tabellenstruktur für Tabelle `deferrals`
+#
+
+CREATE TABLE `deferrals` (
+ `server` varchar(32) NOT NULL default '',
+ `message_id` varchar(16) binary NOT NULL default '',
+ `timestamp` bigint(20) NOT NULL default '0',
+ `rcpt` varchar(200) NOT NULL default '',
+ `rcpt_intermediate` varchar(200) default NULL,
+ `rcpt_final` varchar(200) NOT NULL default '',
+ `host_addr` varchar(15) default NULL,
+ `host_dns` varchar(255) default NULL,
+ `tls_cipher` varchar(128) default NULL,
+ `router` varchar(128) default NULL,
+ `transport` varchar(128) default NULL,
+ `shadow_transport` varchar(128) default NULL,
+ `errmsg` blob,
+ PRIMARY KEY (`server`,`message_id`,`timestamp`,`rcpt`,`rcpt_final`),
+ KEY `rcpt` (`rcpt`),
+ KEY `rcpt_final` (`rcpt_final`),
+ KEY `server` (`server`),
+ KEY `message_id` (`message_id`),
+ KEY `timestamp` (`timestamp`),
+ KEY `host_addr` (`host_addr`)
+) TYPE=MyISAM;
+# --------------------------------------------------------
+
+#
+# Tabellenstruktur für Tabelle `deliveries`
+#
+
+CREATE TABLE `deliveries` (
+ `server` varchar(32) NOT NULL default '',
+ `message_id` varchar(16) binary NOT NULL default '',
+ `timestamp` bigint(20) NOT NULL default '0',
+ `rcpt` varchar(200) NOT NULL default '',
+ `rcpt_intermediate` varchar(200) default NULL,
+ `rcpt_final` varchar(200) NOT NULL default '',
+ `host_addr` varchar(15) default NULL,
+ `host_dns` varchar(255) default NULL,
+ `tls_cipher` varchar(128) default NULL,
+ `router` varchar(128) default NULL,
+ `transport` varchar(128) default NULL,
+ `shadow_transport` varchar(128) default NULL,
+ PRIMARY KEY (`server`,`message_id`,`timestamp`,`rcpt`,`rcpt_final`),
+ KEY `rcpt` (`rcpt`),
+ KEY `rcpt_final` (`rcpt_final`),
+ KEY `host_dns` (`host_dns`),
+ KEY `timestamp` (`timestamp`),
+ KEY `server` (`server`),
+ KEY `message_id` (`message_id`),
+ KEY `host_addr` (`host_addr`)
+) TYPE=MyISAM;
+# --------------------------------------------------------
+
+#
+# Tabellenstruktur für Tabelle `errors`
+#
+
+CREATE TABLE `errors` (
+ `server` varchar(32) NOT NULL default '',
+ `message_id` varchar(16) binary NOT NULL default '',
+ `timestamp` bigint(20) NOT NULL default '0',
+ `rcpt` varchar(200) NOT NULL default '',
+ `rcpt_intermediate` varchar(200) default NULL,
+ `rcpt_final` varchar(200) NOT NULL default '',
+ `host_addr` varchar(15) default NULL,
+ `host_dns` varchar(255) default NULL,
+ `tls_cipher` varchar(128) default NULL,
+ `router` varchar(128) default NULL,
+ `transport` varchar(128) default NULL,
+ `shadow_transport` varchar(128) default NULL,
+ `errmsg` blob,
+ PRIMARY KEY (`server`,`message_id`,`timestamp`,`rcpt`,`rcpt_final`),
+ KEY `timestamp` (`timestamp`),
+ KEY `server` (`server`),
+ KEY `rcpt` (`rcpt`),
+ KEY `host_addr` (`host_addr`),
+ KEY `message_id` (`message_id`),
+ KEY `rcpt_final` (`rcpt_final`)
+) TYPE=MyISAM;
+# --------------------------------------------------------
+
+#
+# Tabellenstruktur für Tabelle `messages`
+#
+
+CREATE TABLE `messages` (
+ `server` varchar(32) NOT NULL default '',
+ `message_id` varchar(16) binary NOT NULL default '',
+ `timestamp` bigint(20) default NULL,
+ `msgid` varchar(255) default NULL,
+ `completed` bigint(20) default NULL,
+ `mailfrom` varchar(255) default NULL,
+ `host_addr` varchar(15) default NULL,
+ `host_rdns` varchar(255) default NULL,
+ `host_ident` varchar(255) default NULL,
+ `host_helo` varchar(255) default NULL,
+ `proto` varchar(32) default NULL,
+ `size` bigint(20) default NULL,
+ `tls_cipher` varchar(128) default NULL,
+ `user` varchar(128) default NULL,
+ `bounce_parent` varchar(16) default NULL,
+ PRIMARY KEY (`server`,`message_id`),
+ KEY `msgid` (`msgid`),
+ KEY `user` (`user`),
+ KEY `timestamp` (`timestamp`),
+ KEY `host_addr` (`host_addr`),
+ KEY `message_id` (`message_id`),
+ KEY `bounce_parent` (`bounce_parent`),
+ KEY `mailfrom` (`mailfrom`),
+ KEY `server` (`server`),
+ KEY `host_dns` (`host_rdns`)
+) TYPE=MyISAM;
+# --------------------------------------------------------
+
+#
+# Tabellenstruktur für Tabelle `queue`
+#
+
+CREATE TABLE `queue` (
+ `server` varchar(32) NOT NULL default '',
+ `message_id` varchar(16) binary NOT NULL default '',
+ `mailfrom` varchar(255) NOT NULL default '',
+ `timestamp` bigint(20) NOT NULL default '0',
+ `num_dsn` int(11) NOT NULL default '0',
+ `frozen` bigint(20) default NULL,
+ `recipients_delivered` blob,
+ `recipients_pending` blob,
+ `spool_path` varchar(64) NOT NULL default '',
+ `subject` varchar(255) default NULL,
+ `msgid` varchar(255) default NULL,
+ `headers` blob NOT NULL,
+ `action` varchar(64) default NULL,
+ PRIMARY KEY (`server`,`message_id`),
+ KEY `spool_path` (`spool_path`),
+ KEY `mailfrom` (`mailfrom`),
+ KEY `message_id` (`message_id`),
+ KEY `server` (`server`),
+ KEY `timestamp` (`timestamp`),
+ KEY `frozen` (`frozen`),
+ KEY `msgid` (`msgid`),
+ KEY `action` (`action`)
+) TYPE=MyISAM;
+# --------------------------------------------------------
+
+#
+# Tabellenstruktur für Tabelle `rejects`
+#
+
+CREATE TABLE `rejects` (
+ `server` varchar(32) NOT NULL default '',
+ `message_id` varchar(16) binary default NULL,
+ `timestamp` bigint(20) NOT NULL default '0',
+ `host_addr` varchar(15) NOT NULL default '',
+ `host_rdns` varchar(255) NOT NULL default '',
+ `host_ident` varchar(255) default NULL,
+ `host_helo` varchar(255) default NULL,
+ `mailfrom` varchar(255) default NULL,
+ `rcpt` varchar(255) default NULL,
+ `errmsg` varchar(255) NOT NULL default '',
+ UNIQUE KEY `rejects_unique` (`server`,`timestamp`,`host_addr`,`errmsg`),
+ KEY `message_id` (`message_id`),
+ KEY `server` (`server`),
+ KEY `timestamp` (`timestamp`),
+ KEY `host_addr` (`host_addr`),
+ KEY `mailfrom` (`mailfrom`),
+ KEY `rcpt` (`rcpt`),
+ KEY `host_dns` (`host_rdns`)
+) TYPE=MyISAM;
+# --------------------------------------------------------
+
+#
+# Tabellenstruktur für Tabelle `unknown`
+#
+
+CREATE TABLE `unknown` (
+ `server` varchar(32) NOT NULL default '',
+ `message_id` varchar(16) binary NOT NULL default '',
+ `timestamp` bigint(20) NOT NULL default '0',
+ `line` varchar(255) NOT NULL default '',
+ PRIMARY KEY (`server`,`message_id`,`timestamp`,`line`),
+ KEY `server` (`server`),
+ KEY `message_id` (`message_id`),
+ KEY `timestamp` (`timestamp`)
+) TYPE=MyISAM;
+
diff --git a/doc/pgsql-db-script.sql b/doc/pgsql-db-script.sql
new file mode 100644
index 0000000..425f2b4
--- /dev/null
+++ b/doc/pgsql-db-script.sql
@@ -0,0 +1,624 @@
+--
+-- PostgreSQL database dump
+--
+
+SET client_encoding = 'LATIN1';
+SET check_function_bodies = false;
+
+--
+-- TOC entry 3 (OID 17145)
+-- Name: exilog; Type: SCHEMA; Schema: -; Owner:
+--
+
+CREATE SCHEMA exilog AUTHORIZATION exilog;
+
+
+SET SESSION AUTHORIZATION 'exilog';
+
+SET search_path = exilog, pg_catalog;
+
+--
+-- TOC entry 4 (OID 17181)
+-- Name: deferrals; Type: TABLE; Schema: exilog; Owner: exilog
+--
+
+CREATE TABLE deferrals (
+ server character varying(32) NOT NULL,
+ message_id character(16) NOT NULL,
+ "timestamp" bigint NOT NULL,
+ rcpt character varying(200) NOT NULL,
+ rcpt_intermediate character varying(200),
+ rcpt_final character varying(200) NOT NULL,
+ host_addr inet,
+ host_dns character varying(255),
+ tls_cipher character varying(128),
+ router character varying(128),
+ transport character varying(128),
+ shadow_transport character varying(128),
+ errmsg character varying(2048)
+);
+
+
+--
+-- TOC entry 5 (OID 17194)
+-- Name: errors; Type: TABLE; Schema: exilog; Owner: exilog
+--
+
+CREATE TABLE errors (
+ server character varying(32) NOT NULL,
+ message_id character(16) NOT NULL,
+ "timestamp" bigint NOT NULL,
+ rcpt character varying(200) NOT NULL,
+ rcpt_intermediate character varying(200),
+ rcpt_final character varying(200) NOT NULL,
+ host_addr inet,
+ host_dns character varying(255),
+ tls_cipher character varying(128),
+ router character varying(128),
+ transport character varying(128),
+ shadow_transport character varying(128),
+ errmsg character varying(2048)
+);
+
+
+--
+-- TOC entry 6 (OID 17207)
+-- Name: deliveries; Type: TABLE; Schema: exilog; Owner: exilog
+--
+
+CREATE TABLE deliveries (
+ server character varying(32) NOT NULL,
+ message_id character(16) NOT NULL,
+ "timestamp" bigint NOT NULL,
+ rcpt character varying(200) NOT NULL,
+ rcpt_intermediate character varying(200),
+ rcpt_final character varying(200) NOT NULL,
+ host_addr inet,
+ host_dns character varying(255),
+ tls_cipher character varying(128),
+ router character varying(128),
+ transport character varying(128),
+ shadow_transport character varying(128)
+);
+
+
+--
+-- TOC entry 7 (OID 17220)
+-- Name: queue; Type: TABLE; Schema: exilog; Owner: exilog
+--
+
+CREATE TABLE queue (
+ server character varying(32) NOT NULL,
+ message_id character(16) NOT NULL,
+ mailfrom character varying(255),
+ "timestamp" bigint,
+ num_dsn integer,
+ frozen bigint,
+ recipients_delivered bytea,
+ recipients_pending bytea,
+ spool_path character varying(64),
+ subject character varying(255),
+ msgid character varying(255),
+ headers bytea,
+ "action" character varying(64)
+);
+
+
+--
+-- TOC entry 8 (OID 17249)
+-- Name: unknown; Type: TABLE; Schema: exilog; Owner: exilog
+--
+
+CREATE TABLE "unknown" (
+ server character varying(32) NOT NULL,
+ message_id character(16) NOT NULL,
+ "timestamp" bigint NOT NULL,
+ line character varying(255) NOT NULL
+);
+
+
+--
+-- TOC entry 9 (OID 695844)
+-- Name: messages; Type: TABLE; Schema: exilog; Owner: exilog
+--
+
+CREATE TABLE messages (
+ server character varying(32) NOT NULL,
+ message_id character(16) NOT NULL,
+ "timestamp" bigint,
+ msgid character varying(255),
+ completed bigint,
+ mailfrom character varying(255),
+ host_addr inet,
+ host_rdns character varying(255),
+ host_ident character varying(255),
+ host_helo character varying(255),
+ proto character varying(32),
+ size bigint,
+ tls_cipher character varying(128),
+ "user" character varying(128),
+ bounce_parent character(16)
+);
+
+
+--
+-- TOC entry 10 (OID 695860)
+-- Name: rejects; Type: TABLE; Schema: exilog; Owner: exilog
+--
+
+CREATE TABLE rejects (
+ server character varying(32) NOT NULL,
+ message_id character(16),
+ "timestamp" bigint NOT NULL,
+ host_addr inet,
+ host_rdns character varying(255),
+ host_ident character varying(255),
+ host_helo character varying(255),
+ mailfrom character varying(255),
+ rcpt character varying(255),
+ errmsg character varying(255) NOT NULL
+);
+
+
+--
+-- TOC entry 16 (OID 17188)
+-- Name: deferrals_server; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deferrals_server ON deferrals USING btree (server);
+
+
+--
+-- TOC entry 12 (OID 17189)
+-- Name: deferrals_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deferrals_message_id ON deferrals USING btree (message_id);
+
+
+--
+-- TOC entry 18 (OID 17190)
+-- Name: deferrals_timestamp; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deferrals_timestamp ON deferrals USING btree ("timestamp");
+
+
+--
+-- TOC entry 14 (OID 17191)
+-- Name: deferrals_rcpt; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deferrals_rcpt ON deferrals USING btree (rcpt);
+
+
+--
+-- TOC entry 15 (OID 17192)
+-- Name: deferrals_rcpt_final; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deferrals_rcpt_final ON deferrals USING btree (rcpt_final);
+
+
+--
+-- TOC entry 11 (OID 17193)
+-- Name: deferrals_host_addr; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deferrals_host_addr ON deferrals USING btree (host_addr);
+
+
+--
+-- TOC entry 24 (OID 17199)
+-- Name: errors_server; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX errors_server ON errors USING btree (server);
+
+
+--
+-- TOC entry 20 (OID 17200)
+-- Name: errors_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX errors_message_id ON errors USING btree (message_id);
+
+
+--
+-- TOC entry 26 (OID 17201)
+-- Name: errors_timestamp; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX errors_timestamp ON errors USING btree ("timestamp");
+
+
+--
+-- TOC entry 22 (OID 17202)
+-- Name: errors_rcpt; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX errors_rcpt ON errors USING btree (rcpt);
+
+
+--
+-- TOC entry 23 (OID 17203)
+-- Name: errors_rcpt_final; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX errors_rcpt_final ON errors USING btree (rcpt_final);
+
+
+--
+-- TOC entry 19 (OID 17204)
+-- Name: errors_host_addr; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX errors_host_addr ON errors USING btree (host_addr);
+
+
+--
+-- TOC entry 32 (OID 17212)
+-- Name: deliveries_server; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deliveries_server ON deliveries USING btree (server);
+
+
+--
+-- TOC entry 28 (OID 17213)
+-- Name: deliveries_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deliveries_message_id ON deliveries USING btree (message_id);
+
+
+--
+-- TOC entry 34 (OID 17214)
+-- Name: deliveries_timestamp; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deliveries_timestamp ON deliveries USING btree ("timestamp");
+
+
+--
+-- TOC entry 30 (OID 17215)
+-- Name: deliveries_rcpt; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deliveries_rcpt ON deliveries USING btree (rcpt);
+
+
+--
+-- TOC entry 31 (OID 17216)
+-- Name: deliveries_rcpt_final; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deliveries_rcpt_final ON deliveries USING btree (rcpt_final);
+
+
+--
+-- TOC entry 27 (OID 17217)
+-- Name: deliveries_host_addr; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deliveries_host_addr ON deliveries USING btree (host_addr);
+
+
+--
+-- TOC entry 41 (OID 17241)
+-- Name: queue_server; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_server ON queue USING btree (server);
+
+
+--
+-- TOC entry 38 (OID 17242)
+-- Name: queue_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_message_id ON queue USING btree (message_id);
+
+
+--
+-- TOC entry 37 (OID 17243)
+-- Name: queue_mailfrom; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_mailfrom ON queue USING btree (mailfrom);
+
+
+--
+-- TOC entry 44 (OID 17244)
+-- Name: queue_timestamp; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_timestamp ON queue USING btree ("timestamp");
+
+
+--
+-- TOC entry 36 (OID 17245)
+-- Name: queue_frozen; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_frozen ON queue USING btree (frozen);
+
+
+--
+-- TOC entry 43 (OID 17246)
+-- Name: queue_spool_path; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_spool_path ON queue USING btree (spool_path);
+
+
+--
+-- TOC entry 39 (OID 17247)
+-- Name: queue_msgid; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_msgid ON queue USING btree (msgid);
+
+
+--
+-- TOC entry 35 (OID 17248)
+-- Name: queue_action; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_action ON queue USING btree ("action");
+
+
+--
+-- TOC entry 47 (OID 17253)
+-- Name: unknown_server; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX unknown_server ON "unknown" USING btree (server);
+
+
+--
+-- TOC entry 45 (OID 17254)
+-- Name: unknown_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX unknown_message_id ON "unknown" USING btree (message_id);
+
+
+--
+-- TOC entry 49 (OID 17255)
+-- Name: unknown_timestamp; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX unknown_timestamp ON "unknown" USING btree ("timestamp");
+
+
+--
+-- TOC entry 17 (OID 237716)
+-- Name: deferrals_server_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deferrals_server_message_id ON deferrals USING btree (server, message_id);
+
+
+--
+-- TOC entry 33 (OID 237717)
+-- Name: deliveries_server_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX deliveries_server_message_id ON deliveries USING btree (server, message_id);
+
+
+--
+-- TOC entry 25 (OID 237719)
+-- Name: errors_server_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX errors_server_message_id ON errors USING btree (server, message_id);
+
+
+--
+-- TOC entry 42 (OID 237725)
+-- Name: queue_server_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX queue_server_message_id ON queue USING btree (server, message_id);
+
+
+--
+-- TOC entry 48 (OID 237821)
+-- Name: unknown_server_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX unknown_server_message_id ON "unknown" USING btree (server, message_id);
+
+
+--
+-- TOC entry 57 (OID 695849)
+-- Name: server; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX server ON messages USING btree (server);
+
+
+--
+-- TOC entry 53 (OID 695850)
+-- Name: message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX message_id ON messages USING btree (message_id);
+
+
+--
+-- TOC entry 55 (OID 695851)
+-- Name: msgid; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX msgid ON messages USING btree (msgid);
+
+
+--
+-- TOC entry 58 (OID 695852)
+-- Name: timestamp; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX "timestamp" ON messages USING btree ("timestamp");
+
+
+--
+-- TOC entry 51 (OID 695853)
+-- Name: host_addr; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX host_addr ON messages USING btree (host_addr);
+
+
+--
+-- TOC entry 50 (OID 695854)
+-- Name: bounce_parent; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX bounce_parent ON messages USING btree (bounce_parent);
+
+
+--
+-- TOC entry 59 (OID 695855)
+-- Name: user; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX "user" ON messages USING btree ("user");
+
+
+--
+-- TOC entry 52 (OID 695856)
+-- Name: mailfrom; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX mailfrom ON messages USING btree (mailfrom);
+
+
+--
+-- TOC entry 54 (OID 695857)
+-- Name: messages_server_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX messages_server_message_id ON messages USING btree (server, message_id);
+
+
+--
+-- TOC entry 64 (OID 695865)
+-- Name: rejects_server; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX rejects_server ON rejects USING btree (server);
+
+
+--
+-- TOC entry 66 (OID 695866)
+-- Name: rejects_timestamp; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX rejects_timestamp ON rejects USING btree ("timestamp");
+
+
+--
+-- TOC entry 60 (OID 695867)
+-- Name: rejects_host_addr; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX rejects_host_addr ON rejects USING btree (host_addr);
+
+
+--
+-- TOC entry 61 (OID 695868)
+-- Name: rejects_mailfrom; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX rejects_mailfrom ON rejects USING btree (mailfrom);
+
+
+--
+-- TOC entry 63 (OID 695869)
+-- Name: rejects_rcpt; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX rejects_rcpt ON rejects USING btree (rcpt);
+
+
+--
+-- TOC entry 62 (OID 695870)
+-- Name: rejects_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX rejects_message_id ON rejects USING btree (message_id);
+
+
+--
+-- TOC entry 65 (OID 695871)
+-- Name: rejects_server_message_id; Type: INDEX; Schema: exilog; Owner: exilog
+--
+
+CREATE INDEX rejects_server_message_id ON rejects USING btree (server, message_id);
+
+
+--
+-- TOC entry 13 (OID 17186)
+-- Name: deferrals_primary; Type: CONSTRAINT; Schema: exilog; Owner: exilog
+--
+
+ALTER TABLE ONLY deferrals
+ ADD CONSTRAINT deferrals_primary PRIMARY KEY (server, message_id, "timestamp", rcpt, rcpt_final);
+
+
+--
+-- TOC entry 21 (OID 17205)
+-- Name: errors_primary; Type: CONSTRAINT; Schema: exilog; Owner: exilog
+--
+
+ALTER TABLE ONLY errors
+ ADD CONSTRAINT errors_primary PRIMARY KEY (server, message_id, "timestamp", rcpt, rcpt_final);
+
+
+--
+-- TOC entry 29 (OID 17218)
+-- Name: deliveries_primary; Type: CONSTRAINT; Schema: exilog; Owner: exilog
+--
+
+ALTER TABLE ONLY deliveries
+ ADD CONSTRAINT deliveries_primary PRIMARY KEY (server, message_id, "timestamp", rcpt, rcpt_final);
+
+
+--
+-- TOC entry 40 (OID 17239)
+-- Name: queue_primary; Type: CONSTRAINT; Schema: exilog; Owner: exilog
+--
+
+ALTER TABLE ONLY queue
+ ADD CONSTRAINT queue_primary PRIMARY KEY (server, message_id);
+
+
+--
+-- TOC entry 46 (OID 17251)
+-- Name: unknown_primary; Type: CONSTRAINT; Schema: exilog; Owner: exilog
+--
+
+ALTER TABLE ONLY "unknown"
+ ADD CONSTRAINT unknown_primary PRIMARY KEY (server, message_id, "timestamp", line);
+
+
+--
+-- TOC entry 56 (OID 695858)
+-- Name: primary; Type: CONSTRAINT; Schema: exilog; Owner: exilog
+--
+
+ALTER TABLE ONLY messages
+ ADD CONSTRAINT "primary" PRIMARY KEY (server, message_id);
+
+
+--
+-- TOC entry 67 (OID 695872)
+-- Name: rejects_unique; Type: CONSTRAINT; Schema: exilog; Owner: exilog
+--
+
+ALTER TABLE ONLY rejects
+ ADD CONSTRAINT rejects_unique UNIQUE (server, "timestamp", host_addr, errmsg);
+
diff --git a/exilog.conf-example b/exilog.conf-example
new file mode 100644
index 0000000..f3d9b60
--- /dev/null
+++ b/exilog.conf-example
@@ -0,0 +1,143 @@
+{ # DO NOT REMOVE THIS BRACKET
+
+ # Exilog config file. Read the comments. Obey the syntax.
+ # (c) Tom Kistner 2005
+
+
+ 'servers' => { # ------------------------------------
+ # Server definitions. One block per server,
+ # separated with comma.
+
+ # Currently, each server only has a single
+ # property: Its group membership. Groups are
+ # just strings that bundle servers. Each
+ # server can only be in one group.
+
+ # Keep the server names short (do not use FQDN).
+ # Likewise, keep the group names short.
+
+ 'foobar' => {
+ 'group' => 'MXes'
+ },
+
+ 'fanucci' => {
+ 'group' => 'MXes'
+ }
+
+ }, # End of server definitions ----------------------
+
+
+ 'sql' => { # ----------------------------------------
+ # SQL Server definition. Use one of the following
+ # blocks as a template.
+
+ # Example for local MySQL server
+ 'type' => 'mysql',
+ 'DBI' => 'DBI:mysql:database=exilog;',
+ 'user' => 'myuser',
+ 'pass' => 'mypass'
+
+ # Example for remote MySQL server
+ #'type' => 'mysql',
+ #'DBI' => 'DBI:mysql:database=exilog;host=foobar.duncanthrax.net;port=3306',
+ #'user' => 'myuser',
+ #'pass' => 'mypass'
+
+ # Example for Postgresql server
+ #'type' => 'pgsql',
+ #'DBI' => 'DBI:Pg:dbname=exilog;host=195.2.162.40;port=5432;',
+ #'user' => 'myuser',
+ #'pass' => 'mypass'
+
+ }, # End of SQL server definition --------------------
+
+
+ 'agent' => { # ---------------------------------------
+ # Agent configuration.
+
+ # The agent writes a log file. You can also
+ # use /dev/null here once things are running
+ # smoothly.
+ 'log' => '/var/log/exilog_agent',
+
+ # The agent writes its PID into this file. Useful,
+ # if you want to start the agent using a command
+ # like start-stop-daemon.
+ 'pidfile' => '/var/run/exilog-agent.pid',
+
+ # If this is set to 'no', the agent will NOT change
+ # its process names to be more informative. This will
+ # prevent problems on systems that restrict changes
+ # to process names for security reasons (Debian and
+ # NetBSD for example).
+ #'use_pretty_names' => 'no',
+
+ # The server the agent is running on. MUST
+ # be one of the names specified in the
+ # 'Servers' section above.
+ 'server' => 'foobar',
+
+ # The log(s) to monitor. If you log via syslog,
+ # this will only be a single file (typically
+ # /var/log/mail). If you use Exim's own logging,
+ # you should specify the mainlog and rejectlog here.
+ 'logs' => [
+ '/var/log/maillog'
+ ],
+
+ # Path to Exim's queue directory.
+ 'queue' => '/var/spool/exim',
+
+ # Path to your Exim binary
+ 'exim' => '/usr/sbin/exim',
+
+ # Delay between two queue listing refreshes.
+ # Thirty seconds is reasonable.
+ 'queue_refresh_delay' => 30
+
+ }, # End of Exilog Agent configuration ---------------
+
+
+ 'cleanup' => { # -------------------------------------
+ # Configuration for the database cleanup tool
+ # (exilog_cleanup.pl).
+
+ # How many days worth of logs to keep in the
+ # database. 10 days is somehow reasonable. If
+ # you run a small shop you can also keep months
+ # of logs. If you run a VERY big shop you might
+ # want to reduce this number or buy some more
+ # processing power.
+ 'cutoff' => 10
+
+ }, # End of exilog_cleanup.pl configuration ----------
+
+
+ 'web' => { # -----------------------------------------
+ # Options for the web interface.
+
+ # Defines how the web interface shows timestamps.
+ # Use 'local' to use the local time of the HTTP server
+ # machine, or use 'gmt' to use normalized GMT
+ # timestamps.
+ # TIP: If all of your machines are in one time zone,
+ # use 'local'.
+ 'timestamps' => 'local',
+
+ # When using basic auth to restrict access to the web
+ # interface, you can define users to be "read-only".
+ # They will not be able to cancel or delete messages
+ # (but they can start a delivery run). Clients that
+ # do not authenticate are mapped to a user name
+ # of "anonymous".
+ 'restricted_users' => [
+ 'anonymous',
+ 'bob',
+ 'alice',
+ 'peter'
+ ]
+
+ } # End of web interface configuration ---------------
+};
+
+# EOF
diff --git a/exilog_agent.pl b/exilog_agent.pl
new file mode 100755
index 0000000..34e1a1f
--- /dev/null
+++ b/exilog_agent.pl
@@ -0,0 +1,471 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+use strict;
+
+use FindBin;
+use FindBin qw($RealBin);
+use lib "$RealBin/";
+
+use exilog_config;
+use exilog_util;
+use POSIX qw( setsid );
+
+use Data::Dumper;
+
+my $foreground = 0;
+$foreground = 1 if (defined($ARGV[0]) && ($ARGV[0] eq '-f'));
+
+
+unless ($foreground) {
+
+ # open log file
+ open(LOG,"> $config->{agent}->{log}")
+ or die "($$) [exilog_agent] Can't open log file $config->{agent}->{log}.\n";
+
+ print LOG scalar localtime()." ($$) [exilog_agent] Starting.\n";
+
+ # fork master process and get rid of the controlling terminal
+ my $rc = fork();
+ if (defined($rc)) {
+ # parent returns
+ if ($rc) {
+ print "($$) [exilog_agent] Detaching from terminal, output goes to $config->{agent}->{log}.\n";
+ exit(0);
+ };
+ }
+ else {
+ print "($$) [exilog_agent] Can't fork!\n";
+ exit(255);
+ };
+
+ setsid();
+
+ # dup STDOUT/ERR
+ open(STDOUT, ">&LOG");
+ open(STDERR, ">&LOG");
+}
+
+$0 = "[exilog_agent]" unless ( edt($config->{agent},'use_pretty_names') &&
+ ($config->{agent}->{use_pretty_names} eq 'no') );
+
+if (exists($config->{agent}->{pidfile})) {
+ open(PID, "> $config->{agent}->{pidfile}")
+ or die "($$) [exilog_agent] Can't open pid-file $config->{agent}->{pidfile}.\n";
+ print PID $$;
+ close(PID);
+}
+
+my @children = ();
+
+# spawn file tailers
+foreach my $logfile (@{ $config->{agent}->{logs} }) {
+ push @children, _tail($logfile);
+};
+
+# spawn queue sync
+push @children, _queue_sync($config->{agent}->{queue});
+
+# spawn queue action child
+push @children, _queue_actions($config->{agent}->{exim});
+
+# set up signal handlers
+$SIG{'HUP'} = \&_terminate;
+$SIG{'INT'} = \&_terminate;
+$SIG{'TERM'} = \&_terminate;
+sub _terminate {
+ kill 15, @children;
+ close(LOG);
+ unlink($config->{agent}->{pidfile}) if (exists($config->{agent}->{pidfile}));
+ exit(0);
+};
+
+# parent process goes to sleep
+while (1) { sleep 10; };
+
+
+sub _queue_actions {
+ my $exim = shift;
+
+ # fork
+ my $rc = fork();
+ if (defined($rc)) {
+ # parent returns
+ if ($rc) {
+ print STDERR "($$) [exilog_agent] spawned queue actions process.\n";
+ return $rc;
+ };
+ }
+ else {
+ print STDERR "($$) [exilog_agent:_queue_actions] Can't fork!\n";
+ exit(255);
+ };
+
+ $0 = "[exilog_agent:_queue_actions] " unless ( edt($config->{agent},'use_pretty_names') &&
+ ($config->{agent}->{use_pretty_names} eq 'no') );
+
+ # open own DB connection
+ use exilog_sql;
+ reconnect();
+
+ # set up warning handler
+ local $SIG{__WARN__} = sub { print STDERR "($$) [exilog_agent:_queue_actions] ".scalar localtime()." ".$_[0] };
+ local $SIG{__DIE__} = sub { print STDERR "($$) [exilog_agent:_queue_actions] ".scalar localtime()." ".$_[0] };
+
+ for (;;) {
+ # conditional reconnect
+ reconnect(1);
+
+ my $deliver = sql_select('queue',
+ [ 'message_id' ],
+ { 'server' => $config->{agent}->{server},
+ 'action' => 'deliver' } );
+
+ my $cancel= sql_select('queue',
+ [ 'message_id' ],
+ { 'server' => $config->{agent}->{server},
+ 'action' => 'cancel' } );
+
+ my $delete = sql_select('queue',
+ [ 'message_id' ],
+ { 'server' => $config->{agent}->{server},
+ 'action' => 'delete' } );
+
+ foreach (@{$deliver}) {
+ system("$exim -qff $_->{message_id} &");
+ sql_queue_clear_action($config->{agent}->{server},$_->{message_id});
+ }
+
+ foreach (@{$cancel}) {
+ system("$exim -Mg $_->{message_id} &");
+ sql_queue_clear_action($config->{agent}->{server},$_->{message_id});
+ }
+
+ foreach (@{$delete}) {
+ system("$exim -Mrm $_->{message_id} &");
+ sql_queue_clear_action($config->{agent}->{server},$_->{message_id});
+ }
+
+ sleep 5;
+ };
+};
+
+
+sub _queue_sync {
+ my $queue = shift;
+
+ # fork
+ my $rc = fork();
+ if (defined($rc)) {
+ # parent returns
+ if ($rc) {
+ print STDERR "($$) [exilog_agent] spawned queue manager process.\n";
+ return $rc;
+ };
+ }
+ else {
+ print STDERR "($$) [exilog_agent:_queue_manager] Can't fork!\n";
+ exit(255);
+ };
+
+ $0 = "[exilog_agent:_queue_manager] ($queue) " unless ( edt($config->{agent},'use_pretty_names') &&
+ ($config->{agent}->{use_pretty_names} eq 'no') );
+ # open own DB connection
+ use exilog_sql;
+ reconnect();
+
+ # set up warning handler
+ local $SIG{__WARN__} = sub { print STDERR "($$) [exilog_agent:_queue_manager] ($queue) ".scalar localtime()." ".$_[0] };
+ local $SIG{__DIE__} = sub { print STDERR "($$) [exilog_agent:_queue_manager] ($queue) ".scalar localtime()." ".$_[0] };
+
+ my $queued = {};
+ for (;;) {
+ # conditional reconnect
+ reconnect(1);
+
+ # build initial file list hash from what we have in the database
+ my $tmp = sql_select('queue',
+ [ 'spool_path' ],
+ { 'server' => $config->{agent}->{server} } );
+
+ foreach (@{ $tmp }) {
+ next if (exists($queued->{$_->{spool_path}}));
+ $queued->{$_->{spool_path}} = 1;
+ };
+
+ my ($created,$updated,$removed) = _queue_read($queue,$queued);
+
+ # remove messages from DB that are not on the queue any more
+ foreach (@{ $removed }) {
+ sql_queue_delete($_);
+ };
+
+ # Manage created and updated messages AFTER our delay, so
+ # short-lived messages do not clutter up the queue table
+ sleep ($config->{agent}->{queue_refresh_delay} || 30);
+
+ # parse created messages and add them to the DB
+ foreach (@{ $created }) {
+ sql_queue_add(_parse_header($queue,$_));
+ };
+
+ # re-parse changes messages and update their db entry
+ foreach (@{ $updated }) {
+ sql_queue_update(_parse_header($queue,$_));
+ };
+ };
+};
+
+sub _parse_header {
+ my $queue = shift;
+ my $path = shift;
+ my $hdr = {};
+
+ return 0 unless open(THIS,"< $queue/$path");
+
+ $hdr->{spool_path} = $path;
+ $hdr->{server} = $config->{agent}->{server};
+ $hdr->{message_id} = <THIS>;
+ chomp($hdr->{message_id});
+ $hdr->{message_id} =~ s/\-H$//;
+
+ <THIS>;
+ $hdr->{mailfrom} = <THIS>;
+ chomp($hdr->{mailfrom});
+ $hdr->{mailfrom} =~ s/^\<//;
+ $hdr->{mailfrom} =~ s/\>$//;
+ $hdr->{mailfrom} = '<>' unless ($hdr->{mailfrom});
+
+ ($hdr->{timestamp},$hdr->{num_dsn}) = split / +/, <THIS>;
+ chomp($hdr->{num_dsn});
+
+ my $line = <THIS>;
+ while ($line =~ /^\-/) {
+ if ($line =~ /^\-acl/) {
+ # swallow ACL variable (those are on extra lines)
+ <THIS>;
+ };
+ if ($line =~ /^\-frozen (.+)$/) {
+ $hdr->{frozen} = $1;
+ chomp($hdr->{frozen});
+ };
+ $line = <THIS>;
+ };
+
+ chomp($line);
+ my $delivered = {};
+ while ($line !~ /^[0-9]+$/) {
+ if ($line !~ /^XX/) {
+ $delivered->{substr($line,3)} = 1;
+ };
+ $line = <THIS>;
+ chomp($line);
+ };
+
+ $line = <THIS>;
+ chomp($line);
+ my @undelivered = ();
+ while ($line) {
+ my @tmp = split / +/, $line;
+ push @undelivered, $tmp[0] unless (exists($delivered->{$tmp[0]}));
+ $line = <THIS>;
+ chomp($line);
+ };
+
+ $hdr->{recipients_delivered} = join(" ",keys %{ $delivered });
+ $hdr->{recipients_pending} = join(" ",@undelivered);
+
+ # finally read headers
+ $hdr->{headers} = "";
+ while(<THIS>) {
+ chomp;
+ if ($_ =~ /[0-9]{3} Subject\: (.+)$/i) {
+ $hdr->{subject} = $1;
+ };
+ if ($_ =~ /[0-9]{3}I Message\-ID\: (.+)$/i) {
+ $hdr->{msgid} = $1;
+ $hdr->{msgid} =~ s/^\<//;
+ $hdr->{msgid} =~ s/\>$//;
+ }
+ if ($_ =~ /^[\t ]/) {
+ $hdr->{headers} .= $_."\n";
+ }
+ else {
+ $hdr->{headers} .= substr($_,5)."\n";
+ };
+ };
+
+ return $hdr;
+};
+
+sub _queue_read {
+ my $queue = shift;
+ my $queued = shift || {};
+
+ my $list = [];
+ _find_headers($queue,"input",$list);
+ _find_headers($queue,"Finput",$list);
+
+ my $created = [];
+ my $updated = [];
+ my $seen = {};
+ foreach my $entry (@{ $list }) {
+ if (exists($queued->{$entry})) {
+ $seen->{$entry} = 1;
+ # was already there, stat it to see if it was updated
+ my $mtime = _mtime($queue."/".$entry);
+ if ($mtime > $queued->{$entry}) {
+ $queued->{$entry} = $mtime;
+ push @{ $updated }, $entry;
+ };
+ }
+ else {
+ # new entry, stat it and add it to the list
+ $queued->{$entry} = _mtime($queue."/".$entry);
+ $seen->{$entry} = 1;
+ push @{ $created }, $entry;
+ };
+ };
+
+ my $removed = [];
+ foreach my $entry (keys %{ $queued } ) {
+ next if (exists($seen->{$entry}));
+ # stale DB entry, delete it
+ delete $queued->{$entry};
+ push @{ $removed }, $entry;
+ };
+
+ return ($created,$updated,$removed);
+};
+
+sub _mtime {
+ my $path = shift;
+ my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
+ $atime,$mtime,$ctime,$blksize,$blocks)
+ = stat($path);
+ return $mtime || 0;
+};
+
+sub _find_headers {
+ my $base = shift;
+ my $subdir = shift;
+ my $list = shift;
+
+ return unless (opendir(THIS,$base."/".$subdir));
+ my @entries = grep !/^\./, readdir THIS;
+ closedir(THIS);
+
+ foreach my $entry (@entries) {
+ if (-d $base."/".$subdir."/".$entry) {
+ _find_headers($base,$subdir."/".$entry,$list);
+ }
+ elsif ($entry =~ /\-H$/) {
+ push @{ $list }, $subdir."/".$entry;
+ }
+ };
+};
+
+
+sub _tail {
+ my $logfile = shift;
+
+ # check if we can open the log file
+ open(LOGFILE,"< $logfile") or do {
+ print STDERR "($$) [exilog_agent:_tail] Can't open logfile '$logfile'.\n";
+ exit(255);
+ };
+ close(LOGFILE);
+
+ # fork
+ my $rc = fork();
+ if (defined($rc)) {
+ # parent returns
+ if ($rc) {
+ print STDERR "($$) [exilog_agent] spawned tail process for '$logfile' ($rc).\n";
+ return $rc;
+ };
+ }
+ else {
+ print STDERR "($$) [exilog_agent:_tail] Can't fork!\n";
+ exit(255);
+ };
+
+ $0 = "[exilog_agent:_tail] ($logfile)" unless ( edt($config->{agent},'use_pretty_names') &&
+ ($config->{agent}->{use_pretty_names} eq 'no') );
+
+ # set up warning handler
+ local $SIG{__WARN__} =
+ sub {
+ return if ($_[0] =~ /Duplicate/i);
+ print STDERR "($$) [exilog_agent:_tail] ($logfile) ".scalar localtime()." ".$_[0];
+ };
+ local $SIG{__DIE__} =
+ sub {
+ return if ($_[0] =~ /Duplicate/i);
+ print STDERR "($$) [exilog_agent:_tail] ($logfile) ".scalar localtime()." ".$_[0];
+ };
+
+ # open the file
+ open(LOGFILE,"< $logfile");
+
+ # import parser, open DB connection
+ use exilog_parse;
+ use exilog_sql;
+ reconnect();
+
+ my $curpos;
+ my $fsize = (-s $logfile);
+ for (;;) {
+ for ($curpos = tell(LOGFILE);
+ $_ = <LOGFILE>;
+ $curpos = tell(LOGFILE)) {
+ my $h = (parse_message_line($_) || parse_reject_line($_));
+ if ($h) {
+ while (!write_message($config->{agent}->{server}, $h)) {
+ # Wait 30 seconds, then reconnect and try again.
+ # If the connect works but writing the line does
+ # not, assume the line is somehow FUBAR and skip
+ # it. If the connect fails, enter 5-minute reconnect
+ # loop.
+ print STDERR "($$) [exilog_agent:_tail] write_message failed. Retrying in 30 seconds.\n";
+ sleep(30);
+ if (reconnect()) {
+ write_message($config->{agent}->{server}, $h);
+ last;
+ }
+ else {
+ while(!reconnect()) {
+ print STDERR "($$) [exilog_agent:_tail] Retrying connection to database.\n";
+ sleep(300);
+ };
+ };
+ };
+ };
+ }
+
+ seek(LOGFILE, $curpos, 0);
+
+ # check if file has been rotated
+ if (-e $logfile) {
+ if ((-s $logfile) < $fsize) {
+ # file is smaller than one second ago
+ print STDERR "($$) [exilog_agent:_tail] File has been rotated, re-opening.\n";
+ close(LOGFILE);
+ open(LOGFILE,"< $logfile");
+ };
+ $fsize = (-s $logfile);
+ }
+
+ # be nice to the CPU
+ sleep(1);
+ }
+};
+
diff --git a/exilog_cgi.pl b/exilog_cgi.pl
new file mode 100755
index 0000000..50344e6
--- /dev/null
+++ b/exilog_cgi.pl
@@ -0,0 +1,124 @@
+#!/usr/bin/perl
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+use strict;
+use exilog_config;
+use exilog_util;
+use exilog_cgi_html;
+use exilog_cgi_param;
+use exilog_sql;
+
+# Put user name into global variable
+my $user = $ENV{'REMOTE_USER'} || 'anonymous';
+
+_print_cgi_headers();
+_print_html_header();
+_print_html_tabs();
+_do_global_actions();
+
+print '<div class="display" align="center">';
+if ($param->{tab} eq 'queues') {
+ use exilog_cgi_queues;
+ queues();
+}
+elsif ($param->{tab} eq 'messages') {
+ use exilog_cgi_messages;
+ messages();
+}
+elsif ($param->{tab} eq 'servers') {
+ use exilog_cgi_servers;
+ servers();
+};
+print '</div>';
+
+_print_html_footer();
+
+
+# -- Private functions ---------------------------------------------------------
+
+sub _do_global_actions {
+
+ # queue actions
+ my $valid_actions = [ 'deliver', 'cancel', 'delete' ];
+ my $restricted_actions = [ 'cancel', 'delete' ];
+ foreach my $p (keys %{ $param }) {
+ if ($p =~ /^ac_([A-Za-z0-9_.-]+?)_([A-Za-z0-9]{6}\-[A-Za-z0-9]{6}-[A-Za-z0-9]{2})$/) {
+ my $server = $1;
+ my $message_id = $2;
+ my $action = $param->{$p};
+ if (ina($valid_actions,$action)) {
+ next if (ina($restricted_actions,$action) && ina($config->{web}->{restricted_users},$main::user));
+ sql_queue_set_action($server,$message_id,$action);
+ }
+ }
+ }
+
+};
+
+
+sub _print_cgi_headers {
+ print $q->header(-expires=>'Thursday, 01-Jan-1970 00:00:01 GMT',
+ -Expires=>'now',
+ -Cache-Control=>'no-cache',
+ -Cache-Control=>'no-store',
+ -Pragma=>'no-cache');
+};
+
+
+sub _print_html_header {
+ print $q->start_html({-title=>"Exilog ".$version,
+ -style=>{-src=>"exilog_stylesheet.css"},
+ -script=>[
+ {-language=>'JAVASCRIPT',
+ -src=>"exilog_jscript.js"},
+ "document.write(getCalendarStyles());"
+ ],
+ -meta=>{'http-equiv' => 'pragma', 'content' => 'no-cache'}});
+ # global "centering" div
+ print $q->start_form({-name=>"exilogform",-method=>"GET"});
+ print '<div align="center">';
+ print '<div align="center" class="body">';
+};
+
+sub _print_html_tabs {
+ my $tabs = { 'servers' => "Servers",
+ 'messages' => "Messages",
+ #'queues' => "Queues", # Queue manager is still unfinished ...
+ 'messages' => "Messages" };
+
+ my $html;
+
+ foreach my $tab (sort keys %{ $tabs }) {
+ $html .= $q->td({-class=>"tabs_spacer"},"&nbsp;").
+ (($param->{"tab"} eq $tab) ?
+ $q->td({-class=>"tabs_active"},$tabs->{$tab})
+ :
+ $q->td({-class=>"tabs_click", -onClick=>"javascript:load_tab('$tab');"},$tabs->{$tab}));
+ };
+
+ print $q->table({-class=>"tabs",-cellpadding=>2,-cellspacing=>0},
+ $q->Tr(
+ $q->td({-align=>"center",-class=>"tabs_static",-style=>"font-size: 16px; font-weight: bold;" }, "Exilog").
+ $html.
+ $q->td({-class=>"tabs_spacer"},"&nbsp;").
+ $q->td({-align=>"center",-class=>"tabs_static",-style=>"font-size: 12px; width: 240px; white-space: nowrap;" }, "&nbsp;&nbsp;Server time".(($config->{web}->{timestamps} eq 'gmt') ? " (GMT)":"").": ".stamp_to_date(time())."&nbsp;&nbsp;")
+ )
+ );
+
+ print $q->input({-type=>"hidden",-name=>"tab",-id=>"tab",-override=>1,-value=>$param->{"tab"}});
+};
+
+
+sub _print_html_footer {
+ print '</div></div>';
+ print $q->end_form();
+ print $q->end_html();
+};
diff --git a/exilog_cgi_html.pm b/exilog_cgi_html.pm
new file mode 100644
index 0000000..f42b795
--- /dev/null
+++ b/exilog_cgi_html.pm
@@ -0,0 +1,984 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_cgi_html;
+use exilog_config;
+use exilog_util;
+use CGI;
+use strict;
+use Data::Dumper;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ &render_message
+ &render_reject
+ &render_queue
+ &render_header
+ &render_server
+ &render_queue_table
+ $q
+
+ );
+
+ %EXPORT_TAGS = ();
+ @EXPORT_OK = qw();
+
+ use vars qw( $q );
+}
+
+$q = new CGI;
+
+# Renders server statistics
+# $stats->{ID}->{order}
+# ->{desc}
+# ->{icon}
+# ->{text}
+
+sub render_server {
+ my $server = shift;
+ my $num_queued = shift;
+ my $h24_stats = shift;
+
+ $q->div({-class=>"top_spacer"},
+ $q->table({-class=>"stats", -cellspacing=>1, -cellpadding=>2, -border=>0},
+ $q->Tr(
+ $q->td({-rowspan=>2,-class=>"table_stats",-style=>"width: 300px;"},
+ $q->table({-cellpadding=>0, -cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-class=>"large_icon"},
+ $q->img({-src=>"icons/server_normal.png",-border=>0})
+ ),
+ $q->td({-class=>"large_text"},
+ $server
+ )
+ )
+ )
+ ),
+ $q->td({-class=>"table_stats"},
+ $q->table({-cellpadding=>0, -cellspacing=>0, -border=>0, -width=>"1%"},
+ $q->Tr(
+ $q->td({-rowspan=>2,-class=>"large_icon"},
+ $q->img({-src=>"icons/queue_normal.png",-border=>0,-title=>"Queue Status"})
+ ),
+ $q->td({-rowspan=>2,-class=>"large_icon"},
+ "<b>Queue Status</b>"
+ ),
+ $q->td({-class=>"stats"},
+ _item( { 'icon' => "icons/queued.png" },
+ {
+ #'link' => { 'tab' => 'queues' },
+ 'text' => ($num_queued->{deferred}+$num_queued->{frozen})." queued (".($num_queued->{deferred_bounce}+$num_queued->{frozen_bounce})." bounces)" } )
+
+ ),
+ $q->td({-class=>"stats"},
+ ( $num_queued->{deferred} ?
+ _item( { 'icon' => "icons/deferred.png" },
+ { 'text' => $num_queued->{deferred}." deferred (".$num_queued->{deferred_bounce}." bounces)" } )
+ :
+ "&nbsp;"
+ )
+ )
+ ),
+ $q->Tr(
+ $q->td({-class=>"stats"},
+ ( $num_queued->{frozen} ?
+ _item( { 'icon' => "icons/frozen.png" },
+ { 'text' => $num_queued->{frozen}." frozen (".$num_queued->{frozen_bounce}." bounces)" } )
+ :
+ "&nbsp;"
+ )
+ )
+ )
+ )
+ )
+ ),
+ $q->Tr(
+ $q->td({-class=>"table_stats"},
+ $q->table({-cellpadding=>0, -cellspacing=>0, -border=>0, -width=>"1%"},
+ $q->Tr(
+ $q->td({-rowspan=>2,-class=>"large_icon"},
+ $q->img({-src=>"icons/stats_h24.png",-border=>0,-title=>"Usage Statistics"})
+ ),
+ $q->td({-rowspan=>2,-class=>"large_icon"},
+ "<b>Last 24h stats</b>"
+ ),
+ $q->td({-class=>"stats"},
+ _item( { 'icon' => "icons/arrival.png", 'title' => "Arrivals" },
+ { 'text' => $h24_stats->{arrivals}." arrivals" } )
+ ),
+ $q->td({-class=>"stats"},
+ _item( { 'icon' => "icons/size.png", 'title' => "Average message size" },
+ { 'text' => "Average message size: ".human_size($h24_stats->{avg_msg_size}) } )
+ )
+ ),
+ $q->Tr(
+ $q->td({-class=>"stats"},
+ _item( { 'icon' => "icons/delivery.png" },
+ { 'text' => $h24_stats->{deliveries}." deliveries" } )
+ ),
+ $q->td({-class=>"stats"},
+ _item( { 'icon' => "icons/error.png" },
+ { 'text' => $h24_stats->{errors}." errors" } )
+ )
+ )
+ )
+ )
+ )
+ )
+ );
+
+};
+
+
+# Renders messages and post-DATA rejects.
+
+sub render_message {
+ my $h = shift; # main message context
+
+ # Subclass list with references to HTML generation code.
+ my $subclasses = { 'rejects' => \&_reject_html,
+ 'deferrals' => \&_deferral_html,
+ 'errors' => \&_error_html,
+ 'deliveries' => \&_delivery_html,
+ 'unknown' => \&_unknown_html,
+ 'queue' => \&_queue_html };
+
+ my $sort_pref = { 'rejects' => 5,
+ 'deferrals' => 4,
+ 'errors' => 3,
+ 'deliveries' => 6,
+ 'unknown' => 2,
+ 'queue' => 1 };
+
+ my @dde = (); # holds list of subclass hashrefs
+ # --->{html} (HTML code generated by )
+ # \->{timestamp} (timestamp for sorting later)
+
+ # Now loop through the subclass list and call HTML
+ # generation code for each entry in all subclasses.
+ # Push the stuff onto dde where we can sort it later.
+ # Remember the timestamp of each entry so we can sort
+ # by it later to display the message events in the
+ # right order.
+ foreach my $subclass (keys %{ $subclasses }) {
+ foreach my $obj (@{ $h->{$subclass} }) {
+ my $tmp = {};
+ $tmp->{timestamp} = $obj->{timestamp};
+ $tmp->{sort_pref} = $sort_pref->{$subclass};
+ # pass in "master sort" timestamp too
+ $tmp->{html} = &{$subclasses->{$subclass}}($obj,$h->{sort_timestamp});
+ push @dde, $tmp;
+ };
+ };
+
+ $q->div({-class=>"top_spacer"},
+ $q->table({-class=>"message", -cellspacing=>1, -cellpadding=>2, -border=>0},
+ _titlebar_html($h),
+ (exists($h->{mailfrom}) ? _message_html($h) : ""),
+ eval {
+ my $event_html = "";
+ foreach my $event (sort by_event_order @dde) {
+ $event_html .= $event->{html};
+ };
+ $event_html;
+ }
+ )
+ );
+};
+sub by_event_order {
+ if ($a->{timestamp} == $b->{timestamp}) {
+ ($a->{sort_pref} <=> $b->{sort_pref});
+ }
+ else {
+ ($a->{timestamp} <=> $b->{timestamp});
+ };
+};
+
+
+# This function is used to render pre-DATA rejects.
+# Since those don't have any other associated events
+# it is useless to go through all other tables like
+# _render_message does.
+
+sub render_reject {
+ my $h = shift;
+ $q->div({-class=>"top_spacer"},
+ $q->table({-class=>"message", -cellspacing=>1, -cellpadding=>2, -border=>0},
+ _titlebar_html($h).
+ _reject_html($h,$h->{timestamp})
+ )
+ );
+};
+
+
+# renders a small "page header"
+
+sub render_header {
+ my $text = shift || "";
+
+ $q->div({-class=>"top_spacer"},
+ $q->table({-class=>"header", -cellspacing=>1, -cellpadding=>2, -border=>0},
+ $q->Tr(
+ $q->td({-class=>"header"},
+ $text
+ )
+ )
+ )
+ );
+}
+
+
+
+sub _titlebar_html {
+ my $h = shift || {};
+
+ my $actions = [ 0, 'deliver' ];
+ unless (ina($config->{web}->{restricted_users}, $main::user)) {
+ if ($h->{mailfrom} ne '<>') {
+ push @{$actions}, 'cancel';
+ }
+ push @{$actions}, 'delete';
+ }
+
+ $q->Tr(
+ $q->td({-class=>"table_titlebar"},
+ $q->table({-cellpadding=>0,-cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-class=>"message_wide"},
+ _item( { 'text' => $h->{server} },
+ ( (edv($h,'message_id') && ($h->{message_id} =~ /^.{6}\-.{6}-.{2}$/) ) ?
+ { 'text' => '&middot;' } : undef ),
+ ( (edv($h,'message_id') && ($h->{message_id} =~ /^.{6}\-.{6}-.{2}$/) ) ?
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'message_id',
+ 'qs' => $h->{message_id} },
+ 'text' => $h->{message_id} } : undef ),
+ ( edv($h,'msgid') ? { 'text' => '&middot;' } : undef ),
+ ( edv($h,'msgid') ? ({ 'link' => { 'tab' => 'messages',
+ 'qt' => 'msgid',
+ 'tr' => '0',
+ 'qs' => $h->{msgid} },
+ 'text' => "Track MSGID" }) : undef ),
+ ( (edv($h,'queue') &&
+ defined(@{$h->{queue}}[0])) ? { 'text' => '&middot;' } : undef ),
+ ( (edv($h,'queue') &&
+ defined(@{$h->{queue}}[0])) ? { 'html' => $q->popup_menu({
+ -name => 'ac_'.$h->{server}.'_'.$h->{'message_id'},
+ -values => $actions,
+ -default => 0,
+ -labels => { 0 => ':: Please select action ::',
+ 'deliver' => 'Force delivery',
+ 'cancel' => 'Cancel (bounce)',
+ 'delete' => 'Delete' },
+ -override => 1
+ }).$q->submit({-name=>'Go'})
+ } : undef ) )
+ ),
+ (exists($h->{size}) ?
+ $q->td({-class=>"message"},
+ _item( { 'icon' => "icons/size.png" },
+ { 'text' => human_size($h->{size})} )
+ ) : ""),
+ (exists($h->{completed}) ?
+ $q->td({-class=>"message"},
+ _item( { 'icon' => "icons/stopwatch.png"},
+ { 'text' => _timespan((defined($h->{completed}) ? $h->{completed} : time()) - $h->{timestamp} ) } )
+ ) : "")
+ )
+ )
+ )
+ );
+};
+
+
+sub _message_html {
+ my $h = shift || {};
+
+ $q->Tr(
+ $q->td({-class=>"table_arrival"},
+ $q->table({-cellpadding=>0,-cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-rowspan=>2,-valign=>"top",-align=>"center",-class=>"large_icon"},
+ ( ($h->{proto} =~ /local/i) ?
+ # local
+ $q->img({-src=>"icons/arrival_local.png",-border=>0,-title=>uc($h->{proto})." | ".$h->{user}})
+ :
+ ( ( ($h->{proto} eq "asmtp") || ($h->{proto} =~ /a$/) ) ?
+ ( defined($h->{tls_cipher}) ?
+ # Auth w/ TLS
+ $q->img({-src=>"icons/arrival_tls_auth.png",-border=>0,-title=>uc($h->{proto})." | ".$h->{user}." | ".$h->{tls_cipher}})
+ :
+ # Auth w/o TLS
+ $q->img({-src=>"icons/arrival_auth.png",-border=>0,-title=>uc($h->{proto})." | ".$h->{user}})
+ )
+ :
+ ( defined($h->{tls_cipher}) ?
+ # TLS
+ $q->img({-src=>"icons/arrival_tls.png",-border=>0,-title=>uc($h->{proto})." | ".$h->{tls_cipher}})
+ :
+ # nothing special
+ $q->img({-src=>"icons/arrival_normal.png",-border=>0,-title=>uc($h->{proto})})
+ )
+ )
+ )
+ ),
+ $q->td(
+ _item( { 'style' => "font-weight: bold;",
+ ( ($h->{mailfrom} eq '<>') ?
+ ( (defined($h->{bounce_parent}) ?
+ ('link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'message_id',
+ 'qs' => $h->{bounce_parent} })
+ : () ),
+ 'text' => "Bounce".
+ (defined($h->{bounce_parent}) ?
+ " of ".$h->{bounce_parent}
+ :
+ ""
+ )
+ )
+ :
+ ('link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $h->{mailfrom} },
+ 'text' => $h->{mailfrom})
+ ) }
+ )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'style' => (($h->{timestamp} == $h->{sort_timestamp}) ? "text-decoration: underline;" : undef) , 'text' => stamp_to_date($h->{timestamp}) },
+ (defined($h->{host_addr}) ? (
+ { 'icon' => "icons/server.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $h->{host_addr} },
+ 'text' => $h->{host_addr} },
+ (edt($h,'host_rdns') ?
+ ( { 'icon' => "icons/dns.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $h->{host_rdns} },
+ 'text' => $h->{host_rdns} } )
+ :
+ ()
+ ),
+ (edt($h,'host_helo') ?
+ ( { 'icon' => "icons/helo.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $h->{host_helo} },
+ 'text' => $h->{host_helo} } )
+ :
+ ()
+ ),
+ (defined($h->{host_ident}) ? (
+ { 'icon' => "icons/ident.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'ident',
+ 'qs' => $h->{host_ident} },
+ 'text' => $h->{host_ident} } )
+ : () )
+ )
+ : () ) )
+ )
+ )
+ )
+ )
+ );
+};
+
+sub _deferral_html {
+ my $deferral = shift || {};
+ my $sort_timestamp = shift || 0;
+
+ $q->Tr(
+ $q->td({-class=>"table_deferral"},
+ $q->table({-cellpadding=>0,-cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-rowspan=>3,-valign=>"top",-align=>"center",-class=>"large_icon"},
+ ( defined($deferral->{tls_cipher}) ?
+ # w/ TLS
+ $q->img({-src=>"icons/deferral_tls.png",-border=>0,-title=>$deferral->{tls_cipher}})
+ :
+ # w/o TLS
+ $q->img({-src=>"icons/deferral_normal.png",-border=>0})
+ )
+ ),
+ $q->td(
+ _item( { 'style' => "font-weight: bold;",
+ 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $deferral->{rcpt} },
+ 'text' => $deferral->{rcpt} },
+ (edt($deferral,'rcpt_intermediate') ?
+ ({ 'text' => '-> '.$deferral->{rcpt_intermediate} })
+ :
+ ()
+ ),
+ ((lc($deferral->{rcpt}) ne lc($deferral->{rcpt_final})) ?
+ ({ 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $deferral->{rcpt_final} },
+ 'text' => '-> '.$deferral->{rcpt_final} })
+ :
+ ()
+ )
+ )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'style' => (($deferral->{timestamp} == $sort_timestamp) ? "text-decoration: underline;" : undef) , 'text' => stamp_to_date($deferral->{timestamp}) },
+ { 'icon' => "icons/router_transport.png" },
+ { 'text' => $deferral->{router}.
+ ( defined($deferral->{transport}) ?
+ "->".$deferral->{transport}.(defined($deferral->{shadow_transport}) ? " [".$deferral->{shadow_transport}."]" : "")
+ :
+ "") },
+ ( defined($deferral->{host_addr}) ?
+ ( { 'icon' => "icons/server.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $deferral->{host_addr} },
+ 'text' => $deferral->{host_addr} },
+ { 'icon' => "icons/dns.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $deferral->{host_dns} },
+ 'text' => $deferral->{host_dns} } )
+ :
+ ()
+ ) )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'icon' => "icons/errmsg.png" },
+ { 'text' => $deferral->{errmsg} } )
+ )
+ )
+ )
+ )
+ );
+};
+
+sub _reject_html {
+ my $reject = shift || {};
+ my $sort_timestamp = shift || 0;
+
+ $q->Tr(
+ $q->td({-class=>"table_reject"},
+ $q->table({-cellpadding=>0,-cellspacing=>0,-border=>0},
+ $q->Tr(
+ $q->td({-rowspan=>2,-valign=>"top",-align=>"center",-class=>"large_icon"},
+ (edv($reject,'message_id') ?
+ # post-DATA
+ $q->img({-src=>"icons/reject_postdata.png",-border=>0})
+ :
+ # pre-DATA
+ $q->img({-src=>"icons/reject_predata.png",-border=>0})
+ )
+ ),
+ $q->td(
+ _item( (edv($reject,'mailfrom') ?
+ (($reject->{mailfrom} eq '<>') ?
+ { 'style' => "font-weight: bold;",
+ 'text' => "Bounce" }
+ :
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $reject->{mailfrom} },
+ 'style' => "font-weight: bold;",
+ 'text' => $reject->{mailfrom} }
+ )
+ :
+ () ),
+ { 'icon' => "icons/server.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $reject->{host_addr} },
+ 'text' => $reject->{host_addr} },
+ (edt($reject,'host_rdns') ?
+ ( { 'icon' => "icons/dns.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $reject->{host_rdns} },
+ 'text' => $reject->{host_rdns} } )
+ :
+ ()
+ ),
+ (edt($reject,'host_helo') ?
+ ( { 'icon' => "icons/helo.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $reject->{host_helo} },
+ 'text' => $reject->{host_helo} } )
+ :
+ ()
+ ),
+ (defined($reject->{host_ident}) ?
+ ( { 'icon' => "icons/ident.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'ident',
+ 'qs' => $reject->{host_ident} },
+ 'text' => $reject->{host_ident} } ) : ()
+ ) )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'style' => (($reject->{timestamp} == $sort_timestamp) ? "text-decoration: underline;" : undef) , 'text' => stamp_to_date($reject->{timestamp}) },
+ { 'icon' => "icons/errmsg.png" },
+ { 'text' => $reject->{errmsg} } )
+ )
+ )
+ )
+ )
+ );
+};
+
+sub _error_html {
+ my $error = shift || {};
+ my $sort_timestamp = shift || 0;
+
+ $q->Tr(
+ $q->td({-class=>"table_error"},
+ $q->table({-cellpadding=>0,-cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-rowspan=>3,-valign=>"top",-align=>"center",-class=>"large_icon"},
+ ( defined($error->{tls_cipher}) ?
+ # w/ TLS
+ $q->img({-src=>"icons/error_tls.png",-border=>0,-title=>$error->{tls_cipher}})
+ :
+ # w/o TLS
+ $q->img({-src=>"icons/error_normal.png",-border=>0})
+ )
+ ),
+ $q->td(
+ _item( { 'style' => "font-weight: bold;",
+ 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $error->{rcpt} },
+ 'text' => $error->{rcpt} },
+ (edt($error,'rcpt_intermediate') ?
+ ({ 'text' => '-> '.$error->{rcpt_intermediate} })
+ :
+ ()
+ ),
+ ((lc($error->{rcpt}) ne lc($error->{rcpt_final})) ?
+ ({ 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $error->{rcpt_final} },
+ 'text' => '-> '.$error->{rcpt_final} })
+ :
+ ()
+ )
+ )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'style' => (($error->{timestamp} == $sort_timestamp) ? "text-decoration: underline;" : undef) , 'text' => stamp_to_date($error->{timestamp}) },
+ ( edv($error,'router') ? (
+ { 'icon' => "icons/router_transport.png" },
+ { 'text' => $error->{router}.
+ ( defined($error->{transport}) ?
+ "->".$error->{transport}.(defined($error->{shadow_transport}) ? " [".$error->{shadow_transport}."]" : "")
+ :
+ "") },
+ ( defined($error->{host_addr}) ?
+ ( { 'icon' => "icons/server.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $error->{host_addr} },
+ 'text' => $error->{host_addr} },
+ { 'icon' => "icons/dns.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $error->{host_dns} },
+ 'text' => $error->{host_dns} } )
+ :
+ ()
+ ) ) : () ) )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'icon' => "icons/errmsg.png" },
+ { 'text' => $error->{errmsg} } )
+ )
+ )
+ )
+ )
+ );
+};
+
+sub _delivery_html {
+ my $delivery = shift || {};
+ my $sort_timestamp = shift || 0;
+
+ $q->Tr(
+ $q->td({-class=>"table_delivery"},
+ $q->table({-cellpadding=>0,-cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-rowspan=>2,-valign=>"top",-align=>"center",-class=>"large_icon"},
+ ( defined($delivery->{tls_cipher}) ?
+ # w/ TLS
+ $q->img({-src=>"icons/delivery_tls.png",-border=>0,-title=>$delivery->{tls_cipher}})
+ :
+ # w/o TLS
+ $q->img({-src=>"icons/delivery_normal.png",-border=>0})
+ )
+ ),
+ $q->td(
+ _item( { 'style' => "font-weight: bold;",
+ 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $delivery->{rcpt} },
+ 'text' => $delivery->{rcpt} },
+ (edt($delivery,'rcpt_intermediate') ?
+ ({ 'text' => '-> '.$delivery->{rcpt_intermediate} })
+ :
+ ()
+ ),
+ ((lc($delivery->{rcpt}) ne lc($delivery->{rcpt_final})) ?
+ ({ 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'addr-all',
+ 'qs' => $delivery->{rcpt_final} },
+ 'text' => '-> '.$delivery->{rcpt_final} })
+ :
+ ()
+ )
+ )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'style' => (($delivery->{timestamp} == $sort_timestamp) ? "text-decoration: underline;" : undef) , 'text' => stamp_to_date($delivery->{timestamp}) },
+ { 'icon' => "icons/router_transport.png" },
+ { 'text' => $delivery->{router}.
+ ( defined($delivery->{transport}) ?
+ "->".$delivery->{transport}.(defined($delivery->{shadow_transport}) ? " [".$delivery->{shadow_transport}."]" : "")
+ :
+ "") },
+ ( defined($delivery->{host_addr}) ?
+ ( { 'icon' => "icons/server.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $delivery->{host_addr} },
+ 'text' => $delivery->{host_addr} },
+ { 'icon' => "icons/dns.png" },
+ { 'link' => { 'tab' => 'messages',
+ 'tr' => '0',
+ 'qt' => 'host-all',
+ 'qs' => $delivery->{host_dns} },
+ 'text' => $delivery->{host_dns} } )
+ :
+ ()
+ ) )
+ )
+ )
+ )
+ )
+ );
+};
+
+sub _unknown_html {
+ my $unknown = shift || {};
+ my $sort_timestamp = shift || 0;
+
+ $q->Tr(
+ $q->td({-class=>"table_unknown"},
+ $q->table({-cellpadding=>0,-cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-valign=>"top",-align=>"center",-class=>"large_icon"},
+ $q->img({-src=>"icons/unknown.png",-border=>0})
+ ),
+ $q->td(
+ _item( { 'style' => (($unknown->{timestamp} == $sort_timestamp) ? "text-decoration: underline;" : undef) , 'text' => stamp_to_date($unknown->{timestamp}) },
+ { 'text' => $unknown->{line} } )
+ )
+ )
+ )
+ )
+ );
+};
+
+sub _queue_html {
+ my $queue = shift || {};
+ my $sort_timestamp = shift || 0;
+
+ my @recipients_delivered = split / /, $queue->{recipients_delivered};
+ my @recipients_pending = split / /, $queue->{recipients_pending};
+
+ $q->Tr(
+ $q->td({-class=>"table_queue"},
+ $q->table({-cellpadding=>0,-cellspacing=>0, -border=>0},
+ $q->Tr(
+ $q->td({-rowspan=>2,-valign=>"top",-align=>"center",-class=>"large_icon"},
+ ( edt($queue,'frozen') ?
+ # frozen
+ $q->img({-src=>"icons/queue_frozen.png",-border=>0,-title=>"Frozen at ".stamp_to_date($queue->{frozen})})
+ :
+ # normal
+ $q->img({-src=>"icons/queue_deferred.png",-border=>0})
+ )
+ ),
+ $q->td(
+ _item( { #'style' => "font-family: Arial, Helvetica, Sans-Serif;",
+ 'text' => $queue->{subject} } )
+ )
+ ),
+ $q->Tr(
+ $q->td(
+ _item( { 'icon' => "icons/delivered.png",
+ ( (scalar @recipients_delivered) ?
+ ( 'title' => join("\n",@recipients_delivered) )
+ :
+ ()
+ ) },
+ { 'text' => scalar @recipients_delivered },
+ { 'text' => '&nbsp;' },
+ { 'icon' => "icons/deferred.png",
+ 'title' => join("\n",@recipients_pending) },
+ { 'text' => scalar @recipients_pending },
+ { 'text' => '&nbsp;' },
+ { 'icon' => "icons/dsn_warning.png",
+ 'title' => "Number of DSNs sent" },
+ { 'text' => $queue->{num_dsn} } )
+ )
+ )
+ )
+ )
+ );
+};
+
+sub render_queue_table {
+ my $messages = shift;
+ my $now = time();
+
+ my $rows = "";
+ foreach my $message (@{ $messages }) {
+ my $row_id = $message->{server}.'_'.$message->{message_id};
+ my @rcpts_delivered = split / /,$message->{recipients_delivered};
+ my @rcpts_pending = split / /,$message->{recipients_pending};
+ $rows .=
+ $q->Tr(
+ $q->td({-class=>"queue"},
+ # Actions
+ ),
+ $q->td({-class=>"queue"},
+ $message->{server}
+ ),
+ $q->td({-class=>"queue"},
+ edt($message,'timestamp') ?
+ _timespan($now - $message->{timestamp})
+ :
+ '?'
+ ),
+ $q->td({-class=>"queue"},
+ _shorten_addr($message->{mailfrom},40)
+ ),
+ $q->td({-class=>"queue",
+ -onMouseOver=>"javascript:document.getElementById('$row_id' + '_pending').style.visibility = 'visible';",
+ -onMouseOut=>"javascript:document.getElementById('$row_id' + '_pending').style.visibility = 'hidden';"},
+ _shorten_addr($rcpts_pending[0],40)
+ ),
+ $q->td({-class=>"queue"},
+ (defined($rcpts_pending[0]) ?
+ $q->div({-id=>$row_id.'_pending', -class=>"rcpts_pending_popup"},"Test<br>Test2<br>Test3")
+ :
+ '').
+ _shorten_string($message->{subject},60)
+ )
+ );
+ };
+
+ $q->div({-class=>"top_spacer"},
+ $q->table({-class=>"queue_table",-cellpadding=>0,-cellspacing=>1,border=>0},
+ $q->Tr(
+ # Table header
+ $q->td({-class=>"queue_header",-width=>"1%"},
+ "&nbsp;"
+ ),
+ $q->td({-class=>"queue_header",-width=>"1%"},
+ "Server"
+ ),
+ $q->td({-class=>"queue_header",-width=>"1%"},
+ "Age"
+ ),
+ $q->td({-class=>"queue_header",-width=>"1%"},
+ "Sender"
+ ),
+ $q->td({-class=>"queue_header",-width=>"1%"},
+ "Recipient(s)"
+ ),
+ $q->td({-class=>"queue_header"},
+ "Subject"
+ )
+ ),
+ $rows
+ )
+ );
+};
+
+
+# -- Private functions -------------------------------------
+
+sub _item {
+ my $html = "";
+
+ # Loop through all parts and build the table TDs
+ while (scalar @_) {
+ my $part = shift @_;
+ next unless $part;
+
+ my $link = "";
+ if (exists($part->{'link'})) {
+ # this item has a link
+ $link = 'exilog_cgi.pl?';
+ foreach my $var (keys %{ $part->{'link'} }) {
+ $link .= $var.'='._url_encode($part->{'link'}->{$var}).'&';
+ }
+ chop($link);
+ }
+
+ if (exists($part->{icon})) {
+ $html .=
+ $q->td({-class=>"item_icon",-style=>(exists($part->{style}) ? $part->{style} : "")},
+ $q->img({ -src=>$part->{icon},
+ -title=>(exists($part->{title}) ? $part->{title} : "" ),
+ -border=>0 })
+ );
+ next;
+ }
+ elsif (exists($part->{html})) {
+ $html .= $q->td({-class=>"item_text"}, $part->{html});
+ }
+ elsif (exists($part->{text})) {
+ # HTML-quote angle brackets
+ $part->{text} =~ s/\>/\&gt\;/g;
+ $part->{text} =~ s/\</\&lt\;/g;
+
+ # break long text at colons or blanks
+ $part->{text} =~ s/([^<>]{80,}?)([: ])/$1$2\<br\>/g;
+
+ $html .=
+ $q->td({-class=>"item_text",
+ ($link ? ( -onClick=>"javascript:document.location.href='$link';",
+ -style=>(exists($part->{style}) ? $part->{style} : "")."cursor:pointer;cursor:hand;",
+ -onMouseOver=>"javascript:link_on(this);",
+ -onMouseOut=>"javascript:link_off(this);" )
+ : (
+ -style=>(exists($part->{style}) ? $part->{style} : "")
+ ) ) },
+ $part->{text}
+ );
+ };
+ };
+
+ # Wrap everything in the surrounding table.
+ return
+ $q->table({-class=>"item",-cellspacing=>0,-cellpadding=>0,-border=>0},
+ $q->Tr(
+ $html
+ )
+ );
+};
+
+
+sub _shorten_addr {
+ my $addr = shift;
+ my $max = shift;
+ return $addr if (length($addr) <= $max);
+
+ my ($localpart,$domain) = split /\@/, $addr, 2;
+
+ if (length($addr) > (int($max/2))) {
+ # shorten local part first
+ $localpart = substr($localpart,0,int($max/4)).'...';
+ };
+ # return if that suffices
+ return $localpart.'@'.$domain if (length($localpart.'@'.$domain) <= $max);
+
+ # shorten domain
+ my @domainparts = split /\./, $domain;
+ while ((scalar @domainparts) > 1) {
+ shift @domainparts;
+ last if (length($localpart.'@'.'...'.join('.',@domainparts)) <= $max);
+ };
+
+ return $localpart.'@'.'...'.join('.',@domainparts);
+};
+
+
+sub _shorten_string {
+ my $string = shift;
+ my $max = shift;
+ return $string if (length($string) <= $max);
+ return substr($string,0,($max-3)).'...';
+};
+
+
+sub _timespan {
+ my $amnt = shift;
+ my @steps = (1,60,60,24,7,999999999);
+ my @units = ('s','m','h','d','wk');
+
+ my $str = "";
+ while ($amnt > $steps[1]) {
+ my $rest = $amnt % $steps[1];
+ $str = $rest.$units[0]." ".$str;
+ $amnt = int($amnt/$steps[1]);
+ shift @units;
+ shift @steps;
+ };
+ $str = $amnt.$units[0]." ".$str;
+ return $str;
+};
+
+sub _url_encode {
+ my $subj = shift;
+ $subj =~ s/([^A-Za-z0-9])/sprintf("%%%02x",ord($1))/eg;
+ return $subj;
+};
+
+1;
diff --git a/exilog_cgi_messages.pm b/exilog_cgi_messages.pm
new file mode 100644
index 0000000..9b36aed
--- /dev/null
+++ b/exilog_cgi_messages.pm
@@ -0,0 +1,816 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_cgi_messages;
+use strict;
+use exilog_config;
+use exilog_cgi_html;
+use exilog_cgi_param;
+use exilog_sql;
+use exilog_util;
+use Net::Netmask;
+use Time::Local;
+
+use Data::Dumper;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ # set the version for version checking
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ &messages
+ );
+
+ %EXPORT_TAGS = ();
+
+ # your exported package globals go here,
+ # as well as any optionally exported functions
+ @EXPORT_OK = qw();
+}
+
+sub _select_all {
+ # All tables
+ my @tables = ( 'deliveries','errors','unknown','deferrals','messages','rejects','queue' );
+ # Only server and timestamp as criteria.
+ # Since these are present on every table
+ # the queries are all the same ...
+ my $criteria = { 'timestamp' => (edt($param,'tr') ? _make_tr() : undef),
+ 'server' => (edt($param,'sr') ? $param->{'sr'} : undef ) };
+
+ my @results = ();
+ foreach my $table (@tables) {
+ next unless (ina($param->{'qw'},$table));
+ push @results, @{ sql_select( $table, [ 'server','message_id','timestamp' ], $criteria ) };
+ };
+ return \@results;
+};
+
+sub _select_ident {
+ if (!edt($param,'qs')) {
+ return [];
+ }
+ my $criteria = { 'timestamp' => (edt($param,'tr') ? _make_tr() : undef),
+ 'server' => (edt($param,'sr') ? $param->{'sr'} : undef ),
+ 'host_ident' => $param->{'qs'} };
+ # Only messages table
+ return sql_select( 'messages', [ 'server','message_id','timestamp' ], $criteria );
+};
+
+sub _select_msgid {
+ if (!edt($param,'qs')) {
+ return [];
+ }
+ # Only messages table
+ return sql_select( 'messages', [ 'server','message_id','timestamp' ], { 'msgid' => $param->{'qs'} } );
+};
+
+sub _select_message_id {
+ if (!edt($param,'qs')) {
+ return [];
+ }
+
+ my @results = ();
+ my @tables = ( 'deliveries','errors','unknown','deferrals','messages','rejects','queue' );
+ my $criteria = { 'message_id' => $param->{'qs'} };
+ foreach my $table (@tables) {
+ push @results, @{ sql_select( $table, [ 'server','message_id','timestamp' ], $criteria ) };
+ };
+
+ # check bounce parent field too
+ push @results, @{ sql_select( 'messages', [ 'server','message_id','timestamp' ], { 'bounce_parent' => $param->{'qs'} } ) };
+
+ return \@results;
+};
+
+sub _select_addr {
+ my $p = shift || 'all';
+
+ if (!edt($param,'qs')) {
+ return [];
+ }
+
+ my @queries;
+ push @queries, { 'table' => 'messages',
+ 'criteria' => { 'mailfrom' => $param->{'qs'} } },
+ { 'table' => 'rejects',
+ 'criteria' => { 'mailfrom' => $param->{'qs'} } }
+ if (($p eq 'sender') || ($p eq 'all'));
+
+ push @queries, { 'table' => 'rejects',
+ 'criteria' => { 'rcpt' => $param->{'qs'} } },
+ { 'table' => 'deliveries',
+ 'criteria' => { 'rcpt' => $param->{'qs'} } },
+ { 'table' => 'deliveries',
+ 'criteria' => { 'rcpt_final' => $param->{'qs'} } },
+ { 'table' => 'deferrals',
+ 'criteria' => { 'rcpt' => $param->{'qs'} } },
+ { 'table' => 'deferrals',
+ 'criteria' => { 'rcpt_final' => $param->{'qs'} } },
+ { 'table' => 'errors',
+ 'criteria' => { 'rcpt' => $param->{'qs'} } },
+ { 'table' => 'errors',
+ 'criteria' => { 'rcpt_final' => $param->{'qs'} } }
+ if (($p eq 'rcpt') || ($p eq 'all'));
+
+
+ my @results = ();
+ foreach my $query (@queries) {
+ next unless (ina($param->{'qw'},$query->{table}));
+ # add standard criteria
+ $query->{criteria}->{'timestamp'} = (edt($param,'tr') ? _make_tr() : undef);
+ $query->{criteria}->{'server'} = (edt($param,'sr') ? $param->{'sr'} : undef );
+ push @results, @{ sql_select( $query->{table}, [ 'server','message_id','timestamp' ], $query->{criteria} ) };
+ };
+
+ return \@results;
+};
+
+
+sub _select_host {
+ my $p = shift || 'all';
+
+ if (!edt($param,'qs')) {
+ return [];
+ }
+
+ my @queries;
+ if ($param->{'qs'} =~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) {
+ # IPv4 address
+ push @queries, { 'table' => 'messages',
+ 'criteria' => { 'host_addr' => $param->{'qs'} } },
+ { 'table' => 'rejects',
+ 'criteria' => { 'host_addr' => $param->{'qs'} } }
+ if (($p eq 'incoming') || ($p eq 'all'));
+
+ push @queries, { 'table' => 'deliveries',
+ 'criteria' => { 'host_addr' => $param->{'qs'} } },
+ { 'table' => 'deferrals',
+ 'criteria' => { 'host_addr' => $param->{'qs'} } },
+ { 'table' => 'errors',
+ 'criteria' => { 'host_addr' => $param->{'qs'} } },
+ { 'table' => 'unknown',
+ 'criteria' => { 'line' => '%'.$param->{'qs'}.'%' } }
+ if (($p eq 'outgoing') || ($p eq 'all'));
+
+ }
+ elsif ($param->{'qs'} =~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\/[0-9]{1,2}$/) {
+ # check if we can make a valid Net::Netmask object out of this
+ my $block = new2 Net::Netmask($param->{'qs'});
+
+ if (!defined($block)) {
+ return "Invalid CIDR specification";
+ };
+
+ # Network specification
+ push @queries, { 'table' => 'messages' },
+ { 'table' => 'rejects' }
+ if (($p eq 'incoming') || ($p eq 'all'));
+
+ push @queries, { 'table' => 'deliveries' },
+ { 'table' => 'deferrals' },
+ { 'table' => 'errors' }
+ if (($p eq 'outgoing') || ($p eq 'all'));
+
+ my @results = ();
+ foreach my $query (@queries) {
+ next unless (ina($param->{'qw'},$query->{table}));
+ # add standard criteria
+ $query->{criteria}->{'timestamp'} = (edt($param,'tr') ? _make_tr() : undef);
+ $query->{criteria}->{'server'} = (edt($param,'sr') ? $param->{'sr'} : undef );
+ push @results, @{ sql_select( $query->{table}, [ 'server','message_id','timestamp','host_addr' ], $query->{criteria} ) };
+ };
+
+ # now weed out those that don't match the CIDR specification
+ my @valid = ();
+ foreach my $result (@results) {
+ if ($block->match($result->{host_addr})) {
+ delete $result->{host_addr};
+ push @valid, $result;
+ };
+ };
+
+ return \@valid;
+ }
+ else {
+ # assume hostname
+ my $prefix_wc = "";
+ my $suffix_wc = "";
+ $prefix_wc = '%' if ($param->{'qs'} !~ /^\%/);
+ $suffix_wc = '%' if ($param->{'qs'} !~ /\%$/);
+
+ push @queries, { 'table' => 'messages',
+ 'criteria' => { 'host_helo' => $param->{'qs'} } },
+ { 'table' => 'messages',
+ 'criteria' => { 'host_rdns' => $param->{'qs'} } },
+ { 'table' => 'rejects',
+ 'criteria' => { 'host_helo' => $param->{'qs'} } },
+ { 'table' => 'rejects',
+ 'criteria' => { 'host_rdns' => $param->{'qs'} } }
+ if (($p eq 'incoming') || ($p eq 'all'));
+
+ push @queries, { 'table' => 'deliveries',
+ 'criteria' => { 'host_dns' => $param->{'qs'} } },
+ { 'table' => 'deferrals',
+ 'criteria' => { 'host_dns' => $param->{'qs'} } },
+ { 'table' => 'errors',
+ 'criteria' => { 'host_dns' => $param->{'qs'} } },
+ { 'table' => 'unknown',
+ # the blank makes sure that we do not match domains in addresses
+ 'criteria' => { 'line' => $prefix_wc.' '.$param->{'qs'}.$suffix_wc } }
+ if (($p eq 'outgoing') || ($p eq 'all'));
+ };
+
+ my @results = ();
+ foreach my $query (@queries) {
+ next unless (ina($param->{'qw'},$query->{table}));
+ # add standard criteria
+ $query->{criteria}->{'timestamp'} = (edt($param,'tr') ? _make_tr() : undef);
+ $query->{criteria}->{'server'} = (edt($param,'sr') ? $param->{'sr'} : undef );
+ push @results, @{ sql_select( $query->{table}, [ 'server','message_id','timestamp' ], $query->{criteria} ) };
+ };
+
+ return \@results;
+};
+
+
+
+sub messages {
+
+ _print_Messages_selector();
+
+ # Check CGI input for event selection.
+ # We need at least a query type ('qt'),
+ # otherwise we only display the selector.
+ my $selected = [];
+ if (edt($param,'qt')) {
+ # Call event selection function for this query type.
+ _print_progress_bar("Collecting message IDs ...");
+ # cut off parameter part (separated with dash)
+ my ($function,$parameter) = split /\-/, $param->{'qt'};
+ no strict "refs";
+ $selected = &{ "_select_".$function }($parameter);
+ if (ref($selected) ne 'ARRAY') {
+ # error
+ _update_progress_bar($selected);
+ return;
+ };
+ }
+ else {
+ # no query type ('qt'), just return
+ return;
+ };
+
+ # Now we have a set of selected messages in an array:
+ #
+ # [0]-->{server}
+ # |->{timestamp}
+ # \->{message_id}
+ # [1]-->{server}
+ # |->{timestamp}
+ # \->{message_id}
+ # ...
+
+ # Perform dupe check. We may have a lot of duplicate IDs
+ # in the list. It is faster to weed them out this way ...
+ _update_progress_bar("Performing dupe check ...");
+ my $dupe = {};
+ my @duped = ();
+ foreach my $message (@{ $selected }) {
+ if (exists($dupe->{$message->{server}}->{$message->{message_id}})) {
+ # Make sure we use the largest timestamp we can find
+ if ($dupe->{$message->{server}}->{$message->{message_id}}->{timestamp} < $message->{timestamp}) {
+ $dupe->{$message->{server}}->{$message->{message_id}}->{timestamp} = $message->{timestamp};
+ };
+ next;
+ };
+ $dupe->{$message->{server}}->{$message->{message_id}} = $message;
+ push @duped, $message;
+ };
+ undef $dupe;
+ undef $selected;
+
+ if ((scalar @duped) == 0) {
+ _update_progress_bar("No matching events found.");
+ return;
+ };
+
+ if (((scalar @duped) > 500) && ($param->{'sm'} !~ /^Confirm/)) {
+ _update_progress_bar("Warning: ".(scalar @duped)." messages/events found. Narrow down your selection or submit the query again.");
+ print '
+ <script language="Javascript">
+ document.forms[0].sm.value = "Confirm Query";
+ </script>
+ ';
+ return;
+ };
+
+ # Initialize stats counters
+ my $stats = {
+ 'num_messages' => { 'desc' => "Messages",
+ 'order' => 1,
+ 'num' => 0 },
+ 'num_rejects' => { 'desc' => "Rejects",
+ 'order' => 5,
+ 'num' => 0 },
+ 'num_deliveries' => { 'desc' => "Deliveries",
+ 'order' => 2,
+ 'num' => 0 },
+ 'num_errors' => { 'desc' => "Errors",
+ 'order' => 3,
+ 'num' => 0 },
+ 'total_turnover' => { 'desc' => "Total Turnover",
+ 'order' => 4,
+ 'size' => 0 }
+ };
+
+ # Now we need to build the complete message set.
+ # This requires a large number of SELECTs.
+ _update_progress_bar("Sorting ...");
+ my $c = 0;
+ foreach my $message (sort { $b->{timestamp} <=> $a->{timestamp} } @duped) {
+
+ # Update the progress bar every 50 entries
+ if (($c % 50) == 0) {
+ _update_progress_bar("Grabbing event data (".$c." of ".scalar @duped." events done) ...");
+ };
+ $c++;
+
+ # Remove timestamp, we'll re-add it later for marking
+ # the "sort" timestamp.
+ my $sort_timestamp = $message->{timestamp};
+ delete $message->{timestamp};
+
+ # Check the message ID.
+ if ($message->{message_id} !~ /^.{6}\-.{6}\-.{2}$/) {
+ # This is a pre-DATA reject/warning.
+ # Render it as a reject.
+ my $complete = @{ sql_select( 'rejects', ['*'], $message ) }[0];
+ $complete->{sort_timestamp} = $sort_timestamp;
+ print render_reject($complete);
+ $stats->{num_rejects}->{num}++;
+ }
+ else {
+ # Try to grab complete arrival ('messages' table)
+ my $complete = @{ sql_select( 'messages', ['*'], $message ) }[0];
+
+ # If there is an arrival, this set has a "real"
+ # message ID. Scan other tables for events.
+ if (defined($complete)) {
+ $complete->{rejects} = sql_select( 'rejects', ['*'], $message );
+ $complete->{deliveries} = sql_select( 'deliveries', ['*'], $message );
+ $complete->{errors} = sql_select( 'errors', ['*'], $message );
+ $complete->{deferrals} = sql_select( 'deferrals', ['*'], $message );
+ $complete->{unknown} = sql_select( 'unknown', ['*'], $message );
+ $complete->{queue} = sql_select( 'queue', ['*'], $message );
+ $complete->{sort_timestamp} = $sort_timestamp;
+ print render_message($complete);
+ $stats->{num_messages}->{num}++;
+ $stats->{total_turnover}->{size} += $complete->{size};
+ $stats->{num_rejects}->{num} += (scalar @{ $complete->{rejects} });
+ $stats->{num_deliveries}->{num} += (scalar @{ $complete->{deliveries} });
+ $stats->{total_turnover}->{size} += ($complete->{size} * (scalar @{ $complete->{deliveries} }));
+ $stats->{num_errors}->{num} += (scalar @{ $complete->{errors} });
+ }
+ # If there is no associated arrival, this is either
+ # a POST-DATA reject (in rejects table) or another
+ # post-DATA warning (in unknown table). Since both
+ # can occur, we render this as a message.
+ else {
+ $complete->{server} = $message->{server};
+ $complete->{message_id} = $message->{message_id};
+ $complete->{rejects} = sql_select( 'rejects', ['*'], $message );
+ $complete->{unknown} = sql_select( 'unknown', ['*'], $message );
+ $complete->{sort_timestamp} = $sort_timestamp;
+ print render_message($complete);
+ $stats->{num_rejects}->{num}++;
+ };
+ };
+ };
+
+ _update_progress_bar(_render_stats($stats));
+};
+
+
+sub _make_tr {
+
+ my $str = $q->param('tr') || 0;
+
+ unless ($str eq 'custom') {
+ my $unit = chop $str;
+ my $now = time();
+ my $units = { '0' => 0,
+ 'm' => 60,
+ 'h' => 3600,
+ 'd' => 86400 };
+ my $then = $now + $units->{$unit}*$str;
+ unless ($now == $then) { # The "unlimited" case
+ $param->{'tds'} = stamp_to_date($then,1);
+ $param->{'tde'} = stamp_to_date($now,1);
+ }
+ return $then;
+ }
+ else {
+ $param->{'tds'} =~ s/ +$//;
+ $param->{'tds'} =~ s/^ +//;
+ $param->{'tde'} =~ s/ +$//;
+ $param->{'tde'} =~ s/^ +//;
+
+ my ($sd,$st) = split / +/, $param->{'tds'};
+ my ($ed,$et) = split / +/, $param->{'tde'};
+
+ if (!$st && $sd =~ /\:/) {
+ $st = $sd;
+ $sd = '';
+ }
+ if (!$et && $ed =~ /\:/) {
+ $et = $ed;
+ $ed = '';
+ }
+
+ $ed = $sd unless($ed);
+
+ my $fsd = _parse_date($sd, $st || '00:00:00');
+ my $fed = _parse_date($ed, $et || '23:59:59');
+
+ $param->{'tds'} = stamp_to_date($fsd);
+ $param->{'tde'} = stamp_to_date($fed);
+
+ return $fsd." ".$fed;
+ }
+}
+
+
+sub _parse_date {
+ my $d = shift;
+ my $t = shift;
+
+ my ($dn,$tn) = split / /, stamp_to_date(time,1);
+ my ($year,$month,$day) = split /\-/, $dn;
+ my ($hour,$minute,$second) = split /\:/, $tn;
+
+ if ($d =~ /^([0-9]{4})\-([0-9]{2})\-([0-9]{2})$/) {
+ $year = $1;
+ $month = $2;
+ $day = $3;
+ }
+ elsif ($d =~ /^([0-9]{2})\-([0-9]{2})$/) {
+ $month = $1;
+ $day = $2;
+ }
+
+ if ($t =~ /^([0-9]{2})\:([0-9]{2})\:([0-9]{2})$/) {
+ $hour = $1;
+ $minute = $2;
+ $second = $3;
+ }
+ elsif ($t =~ /^([0-9]{2})\:([0-9]{2})$/) {
+ $hour = $1;
+ $minute = $2;
+ }
+
+ return date_to_stamp($year.'-'.$month.'-'.$day, $hour.':'.$minute.':'.$second);
+}
+
+sub _render_stats {
+ my $stats = shift || {};
+
+ my @items = ();
+ foreach (sort {$stats->{$a}->{order} <=> $stats->{$b}->{order}} keys %{ $stats }) {
+ if (exists($stats->{$_}->{num}) && $stats->{$_}->{num}) {
+ push @items, $stats->{$_}->{desc}.": ".$stats->{$_}->{num};
+ }
+ elsif (exists($stats->{$_}->{size}) && $stats->{$_}->{size}) {
+ push @items, $stats->{$_}->{desc}.": ".human_size($stats->{$_}->{size});
+ };
+ };
+
+ return join("&nbsp;&nbsp;<b>|</b>&nbsp;&nbsp;",@items);
+};
+
+
+sub _print_progress_bar {
+ my $str = shift || "";
+ print render_header(
+ $q->div({-name=>"progress",-id=>"progress", -align=>"center"},
+ $str
+ )
+ );
+ print "\n<!-- Block filler follows - ".("xxxx" x 1024)." -->\n";
+};
+
+
+sub _update_progress_bar {
+ my $str = shift || "";
+ print '
+ <script language="JavaScript">
+ document.getElementById("progress").innerHTML = "'.$str.'";
+ </script>
+ ';
+ print "\n<!-- Block filler follows - ".("xxxx" x 1024)." -->\n";
+};
+
+
+sub _print_Messages_selector {
+
+ _make_tr();
+
+ print
+ $q->div({-class=>"top_spacer"},
+ $q->div({-align=>"left",-style=>"padding: 10px; border: 1px solid black; background: #eeeeee;"},
+
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0},
+ $q->Tr(
+ $q->td({-align=>"left",-style=>"width: 16px;"},
+ $q->img({-src=>"icons/event_type.png"})
+ ),
+ $q->td({-align=>"left",-style=>"width: 100px;"},
+ "Search Type"
+ ),
+ $q->td({-align=>"left"},
+ $q->popup_menu({ -name=>"qt",
+ -id=>"qt",
+ -style=>"width: 400px;",
+ -values=>[ 'all',
+ 'addr-all',
+ 'addr-sender',
+ 'addr-rcpt',
+ 'host-all',
+ 'host-incoming',
+ 'host-outgoing',
+ 'msgid',
+ 'ident',
+ 'message_id' ],
+ -labels=>{ 'all' => "Show everything",
+ 'addr-all' => "Address (All)",
+ 'addr-sender' => "Address (Sender)",
+ 'addr-rcpt' => "Address (Recipient)",
+ 'host-all' => "Host (all)",
+ 'host-incoming' => "Host (incoming)",
+ 'host-outgoing' => "Host (outgoing)",
+ 'msgid' => "Message-ID (Header)",
+ 'ident' => "Ident String (incoming messages)",
+ 'message_id' => "Message-ID (Exim)"
+ },
+ -default=>(exists($param->{'qt'}) ? ($param->{'qt'} || 'all') : 'all'),
+ -onChange=>"javascript:switch_controls(document.getElementById('qt').options[document.getElementById('qt').selectedIndex].value);",
+ -override=>1})
+ )
+ )
+ )
+ .
+ $q->span({-id=>"term"},'<!-- Dynamic content target DIV -->').
+ $q->div({-id=>"term_hidden",-style=>"visibility: hidden; position: absolute;"},
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0},
+ $q->Tr(
+ $q->td({-align=>"left",-style=>"width: 16px;"},
+ $q->img({-src=>"icons/find.png"})
+ ),
+ $q->td({-align=>"left",-style=>"width: 100px;"},
+ "Search Term"
+ ),
+ $q->td({-align=>"left"},
+ $q->textfield( { -name=>"qs",
+ -style=>"width: 400px;",
+ -value=>(exists($param->{'qs'}) ? ($param->{'qs'} || '') : ''),
+ -override=>1 } )
+ )
+ )
+ )
+ )
+ .
+ $q->span({-id=>"events"},'<!-- Dynamic content target DIV -->').
+ $q->div({-id=>"events_hidden",-style=>"visibility: hidden; position: absolute;"},
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0},
+ $q->Tr(
+ $q->td({-align=>"left",-valign=>"top",-style=>"width: 16px;"},
+ $q->img({-src=>"icons/address.png"})
+ ),
+ $q->td({-align=>"left",-valign=>"top",-style=>"width: 100px;"},
+ "Event types"
+ ),
+ $q->td({-align=>"left",-style=>"padding:2px 4px 4px 4px;"},
+ eval {
+ my @where = ( 'messages',
+ 'errors',
+ 'deliveries',
+ 'deferrals',
+ 'rejects',
+ 'queue'
+ );
+
+ my $labels = { 'messages' => 'Arrivals',
+ 'errors' => 'Errors',
+ 'deliveries' => 'Deliveries',
+ 'deferrals' => 'Deferrals',
+ 'rejects' => 'Rejects',
+ 'queue' => 'Queued' };
+
+ my $html = "";
+ my $num = 0;
+ foreach my $w (@where) {
+ if (($num % 3) == 0) {
+ $html .= '<tr>';
+ };
+ $html .= $q->td({-width=>"1%",-style=>"padding-right: 4px;"},
+ $q->checkbox( { -name=>"qw",
+ -label=>"",
+ -checked=>(ina($param->{'qw'},$w) ? 'checked' : undef),
+ -onDblClick=>"javascript:qw_off_except(this);",
+ -override=>1,
+ -value=>$w } )
+ ).
+ $q->td({-style=>"padding-right: 10px;"},
+ $labels->{$w}
+ );
+ if (($num % 3) == 2) {
+ $html .= '</tr>';
+ };
+ $num++;
+ }
+ $q->table({-border=>0,-cellpadding=>0,-cellspacing=>0,-width=>"1%"},
+ $html
+ );
+ }
+ )
+ )
+ )
+ )
+ .
+ $q->span({-id=>"server"},'<!-- Dynamic content target DIV -->').
+ $q->div({-id=>"server_hidden",-style=>"visibility: hidden; position: absolute;"},
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0},
+ $q->Tr(
+ $q->td({-align=>"left",-valign=>"top",-style=>"width: 16px;"},
+ $q->img({-src=>"icons/server.png"})
+ ),
+ $q->td({-align=>"left",-valign=>"top",-style=>"width: 100px;"},
+ "Servers"
+ ),
+ $q->td({-align=>"left"},
+ eval {
+ my $html ="";
+ my $num = 0;
+ my $groups = {};
+ foreach my $server (sort {$a cmp $b} keys %{ $config->{servers} }) {
+ if (($num % 4) == 0) {
+ $html .= '<tr>';
+ };
+ $html .= $q->td({-width=>"1%",-style=>"padding-right: 4px;"},
+ $q->checkbox( { -name=>"sr",
+ -label=>"",
+ -id=>(edt($config->{servers}->{$server},'group') ? $config->{servers}->{$server}->{group} : "-XXX"),
+ -checked=>(ina($param->{'sr'},$server) ? 'checked' : undef),
+ -override=>1,
+ -onDblClick=>"javascript:sr_off_except(this);",
+ -onChange=>"javascript:sr_changed();",
+ -value=>$server } )
+ ).
+ $q->td({-width=>"1%",-style=>"padding-right: 10px;"},
+ $server
+ );
+ if (($num % 4) == 3) {
+ $html .= '<td>&nbsp;</td></tr>';
+ };
+ $num++;
+ if (edt($config->{servers}->{$server},'group')) {
+ $groups->{$config->{servers}->{$server}->{group}} = '{'.$config->{servers}->{$server}->{group}.'}';
+ };
+ };
+ if (($num % 4) != 0) {
+ $html .= '<td>&nbsp;</td>' x ((4-($num % 4))*2);
+ $html .= '<td>&nbsp;</td></tr>';
+ };
+ $groups->{'-all'} = 'All servers';
+ $groups->{'-custom'} = 'Custom selection';
+ $q->table({-border=>0,-cellpadding=>0,-cellspacing=>0,-width=>"1%"},
+ $q->Tr(
+ $q->td({-colspan=>9,-align=>"left",-style=>"padding-bottom: 4px;"},
+ $q->popup_menu({ -name=>"ss",
+ -id=>"ss",
+ -style=>"width: 400px;",
+ -values=>[ sort {$a cmp $b} keys(%{$groups}) ],
+ -labels=>$groups,
+ -onChange=>"javascript:ss_changed();",
+ -default=>(exists($param->{'ss'}) ? ($param->{'ss'} || '-all') : '-all'),
+ -override=>1})
+ )
+ ),
+ $html
+ );
+ }
+ )
+ )
+ )
+ )
+ .
+ $q->span({-id=>"time"},'<!-- Dynamic content target DIV -->').
+ $q->div({-id=>"time_hidden",-style=>"visibility: hidden; position: absolute;"},
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0},
+ $q->Tr(
+ $q->td({-align=>"left",-style=>"width: 16px;"},
+ $q->img({-src=>"icons/timerange.png"})
+ ),
+ $q->td({-align=>"left",-style=>"width: 100px;"},
+ "Time Range"
+ ),
+ $q->td({-align=>"left"},
+ $q->popup_menu({ -name=>"tr",
+ -id=>"tr",
+ -style=>"width: 125px;",
+ -values=>[ 'custom',
+ '-1m',
+ '-5m',
+ '-10m',
+ '-30m',
+ '-1h',
+ '-6h',
+ '-12h',
+ '-1d',
+ '-2d',
+ '-3d',
+ '-7d',
+ '0' ],
+ -labels=>{ 'custom' => 'Custom',
+ '-1m' => 'Last minute',
+ '-5m' => 'Last 5 minutes',
+ '-10m' => 'Last 10 minutes',
+ '-30m' => 'Last 30 minutes',
+ '-1h' => 'Last hour',
+ '-6h' => 'Last 6 hours',
+ '-12h' => 'Last 12 hours',
+ '-1d' => 'Last 24 hours',
+ '-2d' => 'Last 2 days',
+ '-3d' => 'Last 3 days',
+ '-7d' => 'Last 7 days',
+ '0' => 'Unlimited' },
+ -onChange=>"javascript:document.getElementById('tds').value='';document.getElementById('tde').value='';",
+ -default=>(exists($param->{'tr'}) ? $param->{'tr'} : '-1h'),
+ -override=>1})
+ ),
+ $q->td({-align=>"left",-style=>"padding-right:0px;"},
+ $q->input({-name=>"tds",
+ -style=>"width: 105px;",
+ -value=>(exists($param->{'tds'}) ? $param->{'tds'} : ''),
+ -override=>1,
+ -onFocus=>"javascript:document.getElementById('tr').selectedIndex = 0; document.getElementById('tde').value='';",
+ -id=>"tds" }).
+ $q->button({ -onClick=>"javascript:document.getElementById('tr').selectedIndex = 0; document.getElementById('tde').value=''; cal1x.select(document.forms[0].tds,'anchor1x','yyyy-MM-dd'); return false;",
+ -name=>"X",
+ -style=>"height: 18px; width: 18px;",
+ -id=>"anchor1x" })."&nbsp;-&nbsp;".
+ $q->div({ -id=>'caldiv1x',
+ -style=>"position:absolute;visibility:hidden;background-color:white;layer-background-color:white;" })
+ ),
+ $q->td({-align=>"left",-style=>"padding-left:0px;"},
+ $q->input({-name=>"tde",
+ -style=>"width: 105px;",
+ -value=>(exists($param->{'tde'}) ? $param->{'tde'} : ''),
+ -override=>1,
+ -onFocus=>"javascript:document.getElementById('tr').selectedIndex = 0",
+ -id=>"tde" }).
+ $q->button({ -onClick=>"javascript:document.getElementById('tr').selectedIndex = 0; cal2x.select(document.forms[0].tde,'anchor2x','yyyy-MM-dd'); return false;",
+ -name=>"X",
+ -style=>"height: 18px; width: 18px;",
+ -id=>"anchor2x" }).
+ $q->div({ -id=>'caldiv2x',
+ -style=>"position:absolute;visibility:hidden;background-color:white;layer-background-color:white;" })
+ )
+ )
+ )
+ )
+ .
+ '<hr>'
+ .
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0,-align=>"center"},
+ $q->Tr(
+ $q->td({-align=>"center"},
+ $q->submit({-name=>"sm",-value=>"Start Query"})
+ )
+ )
+ )
+
+ )
+ );
+
+ print "\n".
+ '
+ <script language="JavaScript">
+ init_controls();
+ switch_controls(document.getElementById("qt").options[document.getElementById("qt").selectedIndex].value);
+ </script>
+ '
+ ."\n";
+};
+
+1;
diff --git a/exilog_cgi_param.pm b/exilog_cgi_param.pm
new file mode 100644
index 0000000..5096ee3
--- /dev/null
+++ b/exilog_cgi_param.pm
@@ -0,0 +1,74 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_cgi_param;
+use strict;
+use exilog_cgi_html;
+use exilog_config;
+
+use Data::Dumper;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ $param
+ );
+
+ %EXPORT_TAGS = ();
+ @EXPORT_OK = qw();
+
+ use vars qw( $param );
+}
+
+$param = _init_cgi_params();
+
+sub _init_cgi_params {
+ my $param = {};
+
+ foreach ($q->param) {
+ my @test = $q->param($_);
+
+ if ((scalar @test) > 1) {
+ $param->{$_} = \@test;
+ }
+ else {
+ $param->{$_} = $test[0];
+ };
+ };
+
+ # defaults
+ my $defaults = {
+ 'tab' => 'messages',
+ 'qw' => [ 'messages',
+ 'errors',
+ 'deliveries',
+ 'deferrals',
+ 'rejects',
+ 'queue' ],
+ 'ss' => '-all',
+ 'tr' => '-10m',
+ #'qt' => 'all',
+ 'qs' => "",
+ 'sr' => [ keys %{ $config->{servers} } ]
+ };
+
+ foreach (keys %{ $defaults }) {
+ $param->{$_} = $defaults->{$_} unless exists($param->{$_});
+ };
+ return $param;
+};
+
+
+1;
diff --git a/exilog_cgi_queues.pm b/exilog_cgi_queues.pm
new file mode 100644
index 0000000..ec018a9
--- /dev/null
+++ b/exilog_cgi_queues.pm
@@ -0,0 +1,131 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_cgi_queues;
+use exilog_config;
+use exilog_cgi_html;
+use exilog_cgi_param;
+use exilog_sql;
+use exilog_util;
+use strict;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ # set the version for version checking
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ &queues
+ );
+
+ %EXPORT_TAGS = ();
+
+ # your exported package globals go here,
+ # as well as any optionally exported functions
+ @EXPORT_OK = qw();
+}
+
+
+sub queues {
+ _print_Queue_selector();
+ my $messages = sql_select('queue',[ '*' ]);
+ print render_queue_table($messages);
+};
+
+
+sub _print_Queue_selector {
+
+ print
+ $q->div({-class=>"top_spacer"},
+ $q->div({-align=>"left",-style=>"padding: 10px; border: 1px solid black; background: #eeeeee;"},
+
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0},
+ $q->Tr(
+ $q->td({-align=>"left",-valign=>"top",-style=>"width: 16px;"},
+ $q->img({-src=>"icons/server.png"})
+ ),
+ $q->td({-align=>"left",-valign=>"top",-style=>"width: 100px;"},
+ "Servers"
+ ),
+ $q->td({-align=>"left"},
+ eval {
+ my $html ="";
+ my $num = 0;
+ my $groups = {};
+ foreach my $server (sort {$a cmp $b} keys %{ $config->{servers} }) {
+ if (($num % 4) == 0) {
+ $html .= '<tr>';
+ };
+ $html .= $q->td({-width=>"1%",-style=>"padding-right: 4px;"},
+ $q->checkbox( { -name=>"sr",
+ -label=>"",
+ -id=>(edt($config->{servers}->{$server},'group') ? $config->{servers}->{$server}->{group} : "-XXX"),
+ -checked=>(ina($param->{'sr'},$server) ? 'checked' : undef),
+ -override=>1,
+ -onDblClick=>"javascript:sr_off_except(this);",
+ -onChange=>"javascript:sr_changed();",
+ -value=>$server } )
+ ).
+ $q->td({-width=>"1%",-style=>"padding-right: 10px;"},
+ $server
+ );
+ if (($num % 4) == 3) {
+ $html .= '<td>&nbsp;</td></tr>';
+ };
+ $num++;
+ if (edt($config->{servers}->{$server},'group')) {
+ $groups->{$config->{servers}->{$server}->{group}} = '{'.$config->{servers}->{$server}->{group}.'}';
+ };
+ };
+ if (($num % 4) != 0) {
+ $html .= '<td>&nbsp;</td>' x ((4-($num % 4))*2);
+ $html .= '<td>&nbsp;</td></tr>';
+ };
+ $groups->{'-all'} = 'All servers';
+ $groups->{'-custom'} = 'Custom selection';
+ $q->table({-border=>0,-cellpadding=>0,-cellspacing=>0,-width=>"1%"},
+ $q->Tr(
+ $q->td({-colspan=>9,-align=>"left",-style=>"padding-bottom: 4px;"},
+ $q->popup_menu({ -name=>"ss",
+ -id=>"ss",
+ -style=>"width: 400px;",
+ -values=>[ sort {$a cmp $b} keys(%{$groups}) ],
+ -labels=>$groups,
+ -onChange=>"javascript:ss_changed();",
+ -default=>(exists($param->{'ss'}) ? ($param->{'ss'} || '-all') : '-all'),
+ -override=>1})
+ )
+ ),
+ $html
+ );
+ }.($@ ? $@ : "")
+ )
+ )
+ )
+ .
+ '<hr>'
+ .
+ $q->table({-cellspacing=>0,-cellpadding=>4,-border=>0,-align=>"center"},
+ $q->Tr(
+ $q->td({-align=>"center"},
+ $q->submit({-name=>"sm",-value=>"Start Query"})
+ )
+ )
+ )
+
+ )
+ );
+};
+
+1;
+
diff --git a/exilog_cgi_servers.pm b/exilog_cgi_servers.pm
new file mode 100644
index 0000000..424abf1
--- /dev/null
+++ b/exilog_cgi_servers.pm
@@ -0,0 +1,102 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_cgi_servers;
+use exilog_config;
+use exilog_cgi_html;
+use exilog_cgi_param;
+use exilog_sql;
+use exilog_util;
+use strict;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ # set the version for version checking
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ &servers
+ );
+
+ %EXPORT_TAGS = ();
+
+ # your exported package globals go here,
+ # as well as any optionally exported functions
+ @EXPORT_OK = qw();
+}
+
+
+sub _get_num_queued {
+ my $server = shift;
+ my $h = {};
+
+ $h->{queued} = sql_count('queue',{ 'server' => $server });
+
+ $h->{frozen} = sql_count('queue',{ 'server' => $server,
+ 'frozen' => '1' } );
+
+ $h->{frozen_bounce} = sql_count('queue',{ 'server' => $server,
+ 'mailfrom' => '<>',
+ 'frozen' => '1' } );
+
+ my $tmp = sql_count('queue',{ 'server' => $server,
+ 'mailfrom' => '<>' } );
+
+ $h->{deferred} = $h->{queued} - $h->{frozen};
+ $h->{deferred_bounce} = $tmp - $h->{frozen_bounce};
+
+ return $h;
+}
+
+sub _get_h24_stats {
+ my $server = shift;
+ my $now = time();
+ my $h = {};
+
+ $h->{arrivals} = int( sql_count( 'messages',
+ { 'server' => $server,
+ 'timestamp' => $now-86400 } ) / 1 );
+
+ $h->{deliveries} = int( sql_count( 'deliveries',
+ { 'server' => $server,
+ 'timestamp' => $now-86400 } ) / 1 );
+
+ $h->{errors} = int( sql_count( 'errors',
+ { 'server' => $server,
+ 'timestamp' => $now-86400 } ) / 1 );
+
+ my $sizes = sql_select( 'messages', [ 'size' ], { 'server' => $server,
+ 'timestamp' => $now-86400 } );
+
+ my $total = 0;
+ foreach (@{ $sizes }) { $total+=$_->{size}; };
+ if ((scalar @{ $sizes }) > 0) {
+ $h->{avg_msg_size} = int($total/(scalar @{ $sizes }));
+ }
+ else {
+ $h->{avg_msg_size} = 0;
+ };
+
+ return $h;
+}
+
+sub servers {
+
+ #print $q->div({-style=>"font-size: 28px; font-weight: bold;"},"Basic statictics for all servers");
+
+ foreach my $server (sort {$a cmp $b} keys %{ $config->{servers} }) {
+ print render_server($server,_get_num_queued($server),_get_h24_stats($server));
+ };
+}
+
+1;
diff --git a/exilog_cleanup.pl b/exilog_cleanup.pl
new file mode 100755
index 0000000..5cc3697
--- /dev/null
+++ b/exilog_cleanup.pl
@@ -0,0 +1,65 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+use strict;
+
+use FindBin;
+use FindBin qw($RealBin);
+use lib "$RealBin/";
+
+use exilog_config;
+use exilog_sql;
+
+$0 = "[exilog_cleanup] $config->{cleanup}->{cutoff} days cutoff";
+
+_cleanup($config->{cleanup}->{cutoff});
+
+sub _cleanup {
+ my $days = shift;
+
+ my $now = time();
+ my $cutoff_secs = $now - ($days * 86400);
+
+ print STDERR "($$) [exilog_cleanup] Starting, cutoff date is ".scalar gmtime($cutoff_secs)."\n";
+
+ # remove entries that always have server/message-id
+ my $messages = sql_select('messages',
+ [ 'message_id', 'server' ],
+ { 'completed' => '0 '.$cutoff_secs },
+ undef,undef,undef,undef);
+ print STDERR "($$) [exilog_cleanup] ".(scalar @{ $messages })." messages with completion beyond cutoff date.\n";
+ my $num = 0;
+ foreach (@{ $messages }) {
+ $num += sql_delete('messages', $_);
+ $num += sql_delete('deliveries', $_);
+ $num += sql_delete('deferrals', $_);
+ $num += sql_delete('errors', $_);
+ $num += sql_delete('unknown', $_);
+ };
+ undef $messages;
+ print STDERR "($$) [exilog_cleanup] $num records deleted.\n";
+
+ # remove rejects
+ print STDERR "($$) [exilog_cleanup] cleaning up rejects table.\n";
+ $num = sql_delete('rejects',{ 'timestamp' => '0 '.$cutoff_secs });
+ print STDERR "($$) [exilog_cleanup] $num records deleted.\n";
+
+ # optimize tables
+ print STDERR "($$) [exilog_cleanup] optimizing tables.\n";
+ sql_optimize('messages');
+ sql_optimize('deliveries');
+ sql_optimize('deferrals');
+ sql_optimize('errors');
+ sql_optimize('unknown');
+ sql_optimize('rejects');
+
+ print STDERR "($$) [exilog_cleanup] Done.\n";
+};
diff --git a/exilog_config.pm b/exilog_config.pm
new file mode 100644
index 0000000..47d5483
--- /dev/null
+++ b/exilog_config.pm
@@ -0,0 +1,63 @@
+#!/usr/bin/perl
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_config;
+use strict;
+
+use FindBin;
+use FindBin qw($RealBin);
+use lib "$RealBin/";
+
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ # set the version for version checking
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ $config
+ $version
+ );
+
+ %EXPORT_TAGS = ();
+
+ # your exported package globals go here,
+ # as well as any optionally exported functions
+ @EXPORT_OK = qw();
+
+ use vars qw( $config $version );
+}
+
+$version = "0.5";
+
+$config = _read_ph("$RealBin/exilog.conf");
+
+unless ($config) {
+ print STDERR "($$) [exilog_config] Can't parse configuration file.\n";
+ exit(0);
+};
+
+sub _read_ph {
+ my $file = shift;
+
+ open(PH,"< $file");
+ undef $/;
+ my $tmp = (eval(<PH>));
+ print STDERR "Eval Error: ".$@."\n" if ($@);
+ $/ = "\n";
+ close(PH);
+
+ return $tmp;
+};
+
+1;
diff --git a/exilog_jscript.js b/exilog_jscript.js
new file mode 100644
index 0000000..01aaf52
--- /dev/null
+++ b/exilog_jscript.js
@@ -0,0 +1,1232 @@
+function set_value(myobj,myvalue) {
+ document.getElementById(myobj).value = myvalue;
+};
+
+function load_tab(tab) {
+ set_value('tab',tab);
+ document.exilogform.submit();
+};
+
+function link_on(myobj) {
+ myobj.id = myobj.style.background;
+ myobj.style.background = '#ffcccc';
+};
+
+function link_off(myobj) {
+ myobj.style.background = myobj.id;
+};
+
+var my_controls = new Object;
+my_controls['term'] = new Object;
+my_controls['events'] = new Object;
+my_controls['time'] = new Object;
+my_controls['server'] = new Object;
+
+my_controls['term']['all'] = true;
+
+my_controls['events']['msgid'] = true;
+my_controls['events']['message_id'] = true;
+my_controls['events']['ident'] = true;
+
+my_controls['time']['msgid'] = true;
+my_controls['time']['message_id'] = true;
+
+my_controls['server']['msgid'] = true;
+my_controls['server']['message_id'] = true;
+
+function show_div(mydiv) {
+ if (!my_controls[mydiv]['_is_shown']) {
+ document.getElementById(mydiv).innerHTML = my_controls[mydiv]['_content'];
+ };
+ my_controls[mydiv]['_is_shown'] = true;
+};
+
+function hide_div(mydiv) {
+ if (my_controls[mydiv]['_is_shown']) {
+ my_controls[mydiv]['_content'] = document.getElementById(mydiv).innerHTML;
+ document.getElementById(mydiv).innerHTML = '<!-- Dynamic content target DIV -->';
+ }
+ my_controls[mydiv]['_is_shown'] = false;
+};
+
+function check_control_inactive(mycontrol,myselection) {
+ try {
+ return my_controls[mycontrol][myselection];
+ }
+ catch(error) {}
+ finally {};
+ return false;
+};
+
+function init_controls() {
+ for (var i in my_controls) {
+ my_controls[i]['_content'] = document.getElementById(i + '_hidden').innerHTML;
+ my_controls[i]['_is_shown'] = false;
+ document.getElementById(i + '_hidden').innerHTML = '&nbsp;';
+ };
+};
+
+function switch_controls(myselection) {
+ for (var i in my_controls) {
+ if (check_control_inactive(i,myselection)) {
+ hide_div(i);
+ }
+ else {
+ show_div(i);
+ };
+ }
+};
+
+function check_group(group)
+{
+ var j, c=document.forms[0].elements, s=c.length;
+ for( j=0 ; j<s ; j++ ) {
+ if (c[j].name == "sr")
+ if( c[j].id == group )
+ c[j].checked = true;
+ else
+ c[j].checked = false;
+ };
+}
+
+function ss_changed() {
+ if (document.getElementById('ss').selectedIndex == 0) {
+ // "all" was selected, select all
+ var j, c=document.forms[0].elements, s=c.length;
+ for( j=0 ; j<s ; j++ )
+ if (c[j].name == "sr") c[j].checked = true;
+ }
+ else if (document.getElementById('ss').selectedIndex == 1) {
+ // "custom" was selected, blank all
+ var j, c=document.forms[0].elements, s=c.length;
+ for( j=0 ; j<s ; j++ )
+ if (c[j].name == "sr") c[j].checked = false;
+ }
+ else {
+ var group = document.getElementById('ss').options[document.getElementById('ss').selectedIndex].value;
+ check_group(group);
+ };
+};
+
+function sr_off_except(myobj) {
+ var j, c=document.forms[0].elements, s=c.length;
+ for( j=0 ; j<s ; j++ )
+ if (c[j].name == "sr") c[j].checked = false;
+ myobj.checked = true;
+};
+
+function qw_off_except(myobj) {
+ var j, c=document.forms[0].elements, s=c.length;
+ for( j=0 ; j<s ; j++ )
+ if (c[j].name == "qw") c[j].checked = false;
+ myobj.checked = true;
+};
+
+function sr_changed() {
+ document.getElementById('ss').selectedIndex = 1;
+};
+
+
+
+
+
+// ===================================================================
+// Author: Matt Kruse <matt@mattkruse.com>
+// WWW: http://www.mattkruse.com/
+//
+// NOTICE: You may use this code for any purpose, commercial or
+// private, without any further permission from the author. You may
+// remove this notice from your final code if you wish, however it is
+// appreciated by the author if at least my web site address is kept.
+//
+// You may *NOT* re-distribute this code in any way except through its
+// use. That means, you can include it in your product, or your web
+// site, or any other form where the code is actually being used. You
+// may not put the plain javascript up on your site for download or
+// include it in your javascript libraries for download.
+// If you wish to share this code with others, please just point them
+// to the URL instead.
+// Please DO NOT link directly to my .js files from your site. Copy
+// the files to your server and use them there. Thank you.
+// ===================================================================
+
+
+// getAnchorPosition(anchorname)
+// This function returns an object having .x and .y properties which are the coordinates
+// of the named anchor, relative to the page.
+function getAnchorPosition(anchorname) {
+ // This function will return an Object with x and y properties
+ var useWindow=false;
+ var coordinates=new Object();
+ var x=0,y=0;
+ // Browser capability sniffing
+ var use_gebi=false, use_css=false, use_layers=false;
+ if (document.getElementById) { use_gebi=true; }
+ else if (document.all) { use_css=true; }
+ else if (document.layers) { use_layers=true; }
+ // Logic to find position
+ if (use_gebi && document.all) {
+ x=AnchorPosition_getPageOffsetLeft(document.all[anchorname]);
+ y=AnchorPosition_getPageOffsetTop(document.all[anchorname]);
+ }
+ else if (use_gebi) {
+ var o=document.getElementById(anchorname);
+ x=AnchorPosition_getPageOffsetLeft(o);
+ y=AnchorPosition_getPageOffsetTop(o);
+ }
+ else if (use_css) {
+ x=AnchorPosition_getPageOffsetLeft(document.all[anchorname]);
+ y=AnchorPosition_getPageOffsetTop(document.all[anchorname]);
+ }
+ else if (use_layers) {
+ var found=0;
+ for (var i=0; i<document.anchors.length; i++) {
+ if (document.anchors[i].name==anchorname) { found=1; break; }
+ }
+ if (found==0) {
+ coordinates.x=0; coordinates.y=0; return coordinates;
+ }
+ x=document.anchors[i].x;
+ y=document.anchors[i].y;
+ }
+ else {
+ coordinates.x=0; coordinates.y=0; return coordinates;
+ }
+ coordinates.x=x;
+ coordinates.y=y;
+ return coordinates;
+ }
+
+// getAnchorWindowPosition(anchorname)
+// This function returns an object having .x and .y properties which are the coordinates
+// of the named anchor, relative to the window
+function getAnchorWindowPosition(anchorname) {
+ var coordinates=getAnchorPosition(anchorname);
+ var x=0;
+ var y=0;
+ if (document.getElementById) {
+ if (isNaN(window.screenX)) {
+ x=coordinates.x-document.body.scrollLeft+window.screenLeft;
+ y=coordinates.y-document.body.scrollTop+window.screenTop;
+ }
+ else {
+ x=coordinates.x+window.screenX+(window.outerWidth-window.innerWidth)-window.pageXOffset;
+ y=coordinates.y+window.screenY+(window.outerHeight-24-window.innerHeight)-window.pageYOffset;
+ }
+ }
+ else if (document.all) {
+ x=coordinates.x-document.body.scrollLeft+window.screenLeft;
+ y=coordinates.y-document.body.scrollTop+window.screenTop;
+ }
+ else if (document.layers) {
+ x=coordinates.x+window.screenX+(window.outerWidth-window.innerWidth)-window.pageXOffset;
+ y=coordinates.y+window.screenY+(window.outerHeight-24-window.innerHeight)-window.pageYOffset;
+ }
+ coordinates.x=x;
+ coordinates.y=y;
+ return coordinates;
+ }
+
+// Functions for IE to get position of an object
+function AnchorPosition_getPageOffsetLeft (el) {
+ var ol=el.offsetLeft;
+ while ((el=el.offsetParent) != null) { ol += el.offsetLeft; }
+ return ol;
+ }
+function AnchorPosition_getWindowOffsetLeft (el) {
+ return AnchorPosition_getPageOffsetLeft(el)-document.body.scrollLeft;
+ }
+function AnchorPosition_getPageOffsetTop (el) {
+ var ot=el.offsetTop;
+ while((el=el.offsetParent) != null) { ot += el.offsetTop; }
+ return ot;
+ }
+function AnchorPosition_getWindowOffsetTop (el) {
+ return AnchorPosition_getPageOffsetTop(el)-document.body.scrollTop;
+ }
+
+var MONTH_NAMES=new Array('January','February','March','April','May','June','July','August','September','October','November','December','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
+var DAY_NAMES=new Array('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sun','Mon','Tue','Wed','Thu','Fri','Sat');
+function LZ(x) {return(x<0||x>9?"":"0")+x}
+
+function isDate(val,format) {
+ var date=getDateFromFormat(val,format);
+ if (date==0) { return false; }
+ return true;
+ }
+
+function compareDates(date1,dateformat1,date2,dateformat2) {
+ var d1=getDateFromFormat(date1,dateformat1);
+ var d2=getDateFromFormat(date2,dateformat2);
+ if (d1==0 || d2==0) {
+ return -1;
+ }
+ else if (d1 > d2) {
+ return 1;
+ }
+ return 0;
+ }
+
+function formatDate(date,format) {
+ format=format+"";
+ var result="";
+ var i_format=0;
+ var c="";
+ var token="";
+ var y=date.getYear()+"";
+ var M=date.getMonth()+1;
+ var d=date.getDate();
+ var E=date.getDay();
+ var H=date.getHours();
+ var m=date.getMinutes();
+ var s=date.getSeconds();
+ var yyyy,yy,MMM,MM,dd,hh,h,mm,ss,ampm,HH,H,KK,K,kk,k;
+ // Convert real date parts into formatted versions
+ var value=new Object();
+ if (y.length < 4) {y=""+(y-0+1900);}
+ value["y"]=""+y;
+ value["yyyy"]=y;
+ value["yy"]=y.substring(2,4);
+ value["M"]=M;
+ value["MM"]=LZ(M);
+ value["MMM"]=MONTH_NAMES[M-1];
+ value["NNN"]=MONTH_NAMES[M+11];
+ value["d"]=d;
+ value["dd"]=LZ(d);
+ value["E"]=DAY_NAMES[E+7];
+ value["EE"]=DAY_NAMES[E];
+ value["H"]=H;
+ value["HH"]=LZ(H);
+ if (H==0){value["h"]=12;}
+ else if (H>12){value["h"]=H-12;}
+ else {value["h"]=H;}
+ value["hh"]=LZ(value["h"]);
+ if (H>11){value["K"]=H-12;} else {value["K"]=H;}
+ value["k"]=H+1;
+ value["KK"]=LZ(value["K"]);
+ value["kk"]=LZ(value["k"]);
+ if (H > 11) { value["a"]="PM"; }
+ else { value["a"]="AM"; }
+ value["m"]=m;
+ value["mm"]=LZ(m);
+ value["s"]=s;
+ value["ss"]=LZ(s);
+ while (i_format < format.length) {
+ c=format.charAt(i_format);
+ token="";
+ while ((format.charAt(i_format)==c) && (i_format < format.length)) {
+ token += format.charAt(i_format++);
+ }
+ if (value[token] != null) { result=result + value[token]; }
+ else { result=result + token; }
+ }
+ return result;
+ }
+
+function _isInteger(val) {
+ var digits="1234567890";
+ for (var i=0; i < val.length; i++) {
+ if (digits.indexOf(val.charAt(i))==-1) { return false; }
+ }
+ return true;
+ }
+function _getInt(str,i,minlength,maxlength) {
+ for (var x=maxlength; x>=minlength; x--) {
+ var token=str.substring(i,i+x);
+ if (token.length < minlength) { return null; }
+ if (_isInteger(token)) { return token; }
+ }
+ return null;
+ }
+
+function getDateFromFormat(val,format) {
+ val=val+"";
+ format=format+"";
+ var i_val=0;
+ var i_format=0;
+ var c="";
+ var token="";
+ var token2="";
+ var x,y;
+ var now=new Date();
+ var year=now.getYear();
+ var month=now.getMonth()+1;
+ var date=1;
+ var hh=now.getHours();
+ var mm=now.getMinutes();
+ var ss=now.getSeconds();
+ var ampm="";
+
+ while (i_format < format.length) {
+ // Get next token from format string
+ c=format.charAt(i_format);
+ token="";
+ while ((format.charAt(i_format)==c) && (i_format < format.length)) {
+ token += format.charAt(i_format++);
+ }
+ // Extract contents of value based on format token
+ if (token=="yyyy" || token=="yy" || token=="y") {
+ if (token=="yyyy") { x=4;y=4; }
+ if (token=="yy") { x=2;y=2; }
+ if (token=="y") { x=2;y=4; }
+ year=_getInt(val,i_val,x,y);
+ if (year==null) { return 0; }
+ i_val += year.length;
+ if (year.length==2) {
+ if (year > 70) { year=1900+(year-0); }
+ else { year=2000+(year-0); }
+ }
+ }
+ else if (token=="MMM"||token=="NNN"){
+ month=0;
+ for (var i=0; i<MONTH_NAMES.length; i++) {
+ var month_name=MONTH_NAMES[i];
+ if (val.substring(i_val,i_val+month_name.length).toLowerCase()==month_name.toLowerCase()) {
+ if (token=="MMM"||(token=="NNN"&&i>11)) {
+ month=i+1;
+ if (month>12) { month -= 12; }
+ i_val += month_name.length;
+ break;
+ }
+ }
+ }
+ if ((month < 1)||(month>12)){return 0;}
+ }
+ else if (token=="EE"||token=="E"){
+ for (var i=0; i<DAY_NAMES.length; i++) {
+ var day_name=DAY_NAMES[i];
+ if (val.substring(i_val,i_val+day_name.length).toLowerCase()==day_name.toLowerCase()) {
+ i_val += day_name.length;
+ break;
+ }
+ }
+ }
+ else if (token=="MM"||token=="M") {
+ month=_getInt(val,i_val,token.length,2);
+ if(month==null||(month<1)||(month>12)){return 0;}
+ i_val+=month.length;}
+ else if (token=="dd"||token=="d") {
+ date=_getInt(val,i_val,token.length,2);
+ if(date==null||(date<1)||(date>31)){return 0;}
+ i_val+=date.length;}
+ else if (token=="hh"||token=="h") {
+ hh=_getInt(val,i_val,token.length,2);
+ if(hh==null||(hh<1)||(hh>12)){return 0;}
+ i_val+=hh.length;}
+ else if (token=="HH"||token=="H") {
+ hh=_getInt(val,i_val,token.length,2);
+ if(hh==null||(hh<0)||(hh>23)){return 0;}
+ i_val+=hh.length;}
+ else if (token=="KK"||token=="K") {
+ hh=_getInt(val,i_val,token.length,2);
+ if(hh==null||(hh<0)||(hh>11)){return 0;}
+ i_val+=hh.length;}
+ else if (token=="kk"||token=="k") {
+ hh=_getInt(val,i_val,token.length,2);
+ if(hh==null||(hh<1)||(hh>24)){return 0;}
+ i_val+=hh.length;hh--;}
+ else if (token=="mm"||token=="m") {
+ mm=_getInt(val,i_val,token.length,2);
+ if(mm==null||(mm<0)||(mm>59)){return 0;}
+ i_val+=mm.length;}
+ else if (token=="ss"||token=="s") {
+ ss=_getInt(val,i_val,token.length,2);
+ if(ss==null||(ss<0)||(ss>59)){return 0;}
+ i_val+=ss.length;}
+ else if (token=="a") {
+ if (val.substring(i_val,i_val+2).toLowerCase()=="am") {ampm="AM";}
+ else if (val.substring(i_val,i_val+2).toLowerCase()=="pm") {ampm="PM";}
+ else {return 0;}
+ i_val+=2;}
+ else {
+ if (val.substring(i_val,i_val+token.length)!=token) {return 0;}
+ else {i_val+=token.length;}
+ }
+ }
+ // If there are any trailing characters left in the value, it doesn't match
+ if (i_val != val.length) { return 0; }
+ // Is date valid for month?
+ if (month==2) {
+ // Check for leap year
+ if ( ( (year%4==0)&&(year%100 != 0) ) || (year%400==0) ) { // leap year
+ if (date > 29){ return 0; }
+ }
+ else { if (date > 28) { return 0; } }
+ }
+ if ((month==4)||(month==6)||(month==9)||(month==11)) {
+ if (date > 30) { return 0; }
+ }
+ // Correct hours value
+ if (hh<12 && ampm=="PM") { hh=hh-0+12; }
+ else if (hh>11 && ampm=="AM") { hh-=12; }
+ var newdate=new Date(year,month-1,date,hh,mm,ss);
+ return newdate.getTime();
+ }
+
+function parseDate(val) {
+ var preferEuro=(arguments.length==2)?arguments[1]:false;
+ generalFormats=new Array('y-M-d','MMM d, y','MMM d,y','y-MMM-d','d-MMM-y','MMM d');
+ monthFirst=new Array('M/d/y','M-d-y','M.d.y','MMM-d','M/d','M-d');
+ dateFirst =new Array('d/M/y','d-M-y','d.M.y','d-MMM','d/M','d-M');
+ var checkList=new Array('generalFormats',preferEuro?'dateFirst':'monthFirst',preferEuro?'monthFirst':'dateFirst');
+ var d=null;
+ for (var i=0; i<checkList.length; i++) {
+ var l=window[checkList[i]];
+ for (var j=0; j<l.length; j++) {
+ d=getDateFromFormat(val,l[j]);
+ if (d!=0) { return new Date(d); }
+ }
+ }
+ return null;
+ }
+
+
+function PopupWindow_getXYPosition(anchorname) {
+ var coordinates;
+ if (this.type == "WINDOW") {
+ coordinates = getAnchorWindowPosition(anchorname);
+ }
+ else {
+ coordinates = getAnchorPosition(anchorname);
+ }
+ this.x = coordinates.x;
+ this.y = coordinates.y;
+ }
+function PopupWindow_setSize(width,height) {
+ this.width = width;
+ this.height = height;
+ }
+function PopupWindow_populate(contents) {
+ this.contents = contents;
+ this.populated = false;
+ }
+function PopupWindow_setUrl(url) {
+ this.url = url;
+ }
+function PopupWindow_setWindowProperties(props) {
+ this.windowProperties = props;
+ }
+function PopupWindow_refresh() {
+ if (this.divName != null) {
+ // refresh the DIV object
+ if (this.use_gebi) {
+ document.getElementById(this.divName).innerHTML = this.contents;
+ }
+ else if (this.use_css) {
+ document.all[this.divName].innerHTML = this.contents;
+ }
+ else if (this.use_layers) {
+ var d = document.layers[this.divName];
+ d.document.open();
+ d.document.writeln(this.contents);
+ d.document.close();
+ }
+ }
+ else {
+ if (this.popupWindow != null && !this.popupWindow.closed) {
+ if (this.url!="") {
+ this.popupWindow.location.href=this.url;
+ }
+ else {
+ this.popupWindow.document.open();
+ this.popupWindow.document.writeln(this.contents);
+ this.popupWindow.document.close();
+ }
+ this.popupWindow.focus();
+ }
+ }
+ }
+function PopupWindow_showPopup(anchorname) {
+ this.getXYPosition(anchorname);
+ this.x += this.offsetX;
+ this.y += this.offsetY;
+ if (!this.populated && (this.contents != "")) {
+ this.populated = true;
+ this.refresh();
+ }
+ if (this.divName != null) {
+ // Show the DIV object
+ if (this.use_gebi) {
+ document.getElementById(this.divName).style.left = this.x + "px";
+ document.getElementById(this.divName).style.top = this.y + "px";
+ document.getElementById(this.divName).style.visibility = "visible";
+ }
+ else if (this.use_css) {
+ document.all[this.divName].style.left = this.x;
+ document.all[this.divName].style.top = this.y;
+ document.all[this.divName].style.visibility = "visible";
+ }
+ else if (this.use_layers) {
+ document.layers[this.divName].left = this.x;
+ document.layers[this.divName].top = this.y;
+ document.layers[this.divName].visibility = "visible";
+ }
+ }
+ else {
+ if (this.popupWindow == null || this.popupWindow.closed) {
+ // If the popup window will go off-screen, move it so it doesn't
+ if (this.x<0) { this.x=0; }
+ if (this.y<0) { this.y=0; }
+ if (screen && screen.availHeight) {
+ if ((this.y + this.height) > screen.availHeight) {
+ this.y = screen.availHeight - this.height;
+ }
+ }
+ if (screen && screen.availWidth) {
+ if ((this.x + this.width) > screen.availWidth) {
+ this.x = screen.availWidth - this.width;
+ }
+ }
+ var avoidAboutBlank = window.opera || ( document.layers && !navigator.mimeTypes['*'] ) || navigator.vendor == 'KDE' || ( document.childNodes && !document.all && !navigator.taintEnabled );
+ this.popupWindow = window.open(avoidAboutBlank?"":"about:blank","window_"+anchorname,this.windowProperties+",width="+this.width+",height="+this.height+",screenX="+this.x+",left="+this.x+",screenY="+this.y+",top="+this.y+"");
+ }
+ this.refresh();
+ }
+ }
+function PopupWindow_hidePopup() {
+ if (this.divName != null) {
+ if (this.use_gebi) {
+ document.getElementById(this.divName).style.visibility = "hidden";
+ }
+ else if (this.use_css) {
+ document.all[this.divName].style.visibility = "hidden";
+ }
+ else if (this.use_layers) {
+ document.layers[this.divName].visibility = "hidden";
+ }
+ }
+ else {
+ if (this.popupWindow && !this.popupWindow.closed) {
+ this.popupWindow.close();
+ this.popupWindow = null;
+ }
+ }
+ }
+// Pass an event and return whether or not it was the popup DIV that was clicked
+function PopupWindow_isClicked(e) {
+ if (this.divName != null) {
+ if (this.use_layers) {
+ var clickX = e.pageX;
+ var clickY = e.pageY;
+ var t = document.layers[this.divName];
+ if ((clickX > t.left) && (clickX < t.left+t.clip.width) && (clickY > t.top) && (clickY < t.top+t.clip.height)) {
+ return true;
+ }
+ else { return false; }
+ }
+ else if (document.all) { // Need to hard-code this to trap IE for error-handling
+ var t = window.event.srcElement;
+ while (t.parentElement != null) {
+ if (t.id==this.divName) {
+ return true;
+ }
+ t = t.parentElement;
+ }
+ return false;
+ }
+ else if (this.use_gebi && e) {
+ var t = e.originalTarget;
+ while (t.parentNode != null) {
+ if (t.id==this.divName) {
+ return true;
+ }
+ t = t.parentNode;
+ }
+ return false;
+ }
+ return false;
+ }
+ return false;
+ }
+
+// Check an onMouseDown event to see if we should hide
+function PopupWindow_hideIfNotClicked(e) {
+ if (this.autoHideEnabled && !this.isClicked(e)) {
+ this.hidePopup();
+ }
+ }
+// Call this to make the DIV disable automatically when mouse is clicked outside it
+function PopupWindow_autoHide() {
+ this.autoHideEnabled = true;
+ }
+// This global function checks all PopupWindow objects onmouseup to see if they should be hidden
+function PopupWindow_hidePopupWindows(e) {
+ for (var i=0; i<popupWindowObjects.length; i++) {
+ if (popupWindowObjects[i] != null) {
+ var p = popupWindowObjects[i];
+ p.hideIfNotClicked(e);
+ }
+ }
+ }
+// Run this immediately to attach the event listener
+function PopupWindow_attachListener() {
+ if (document.layers) {
+ document.captureEvents(Event.MOUSEUP);
+ }
+ window.popupWindowOldEventListener = document.onmouseup;
+ if (window.popupWindowOldEventListener != null) {
+ document.onmouseup = new Function("window.popupWindowOldEventListener(); PopupWindow_hidePopupWindows();");
+ }
+ else {
+ document.onmouseup = PopupWindow_hidePopupWindows;
+ }
+ }
+// CONSTRUCTOR for the PopupWindow object
+// Pass it a DIV name to use a DHTML popup, otherwise will default to window popup
+function PopupWindow() {
+ if (!window.popupWindowIndex) { window.popupWindowIndex = 0; }
+ if (!window.popupWindowObjects) { window.popupWindowObjects = new Array(); }
+ if (!window.listenerAttached) {
+ window.listenerAttached = true;
+ PopupWindow_attachListener();
+ }
+ this.index = popupWindowIndex++;
+ popupWindowObjects[this.index] = this;
+ this.divName = null;
+ this.popupWindow = null;
+ this.width=0;
+ this.height=0;
+ this.populated = false;
+ this.visible = false;
+ this.autoHideEnabled = false;
+
+ this.contents = "";
+ this.url="";
+ this.windowProperties="toolbar=no,location=no,status=no,menubar=no,scrollbars=auto,resizable,alwaysRaised,dependent,titlebar=no";
+ if (arguments.length>0) {
+ this.type="DIV";
+ this.divName = arguments[0];
+ }
+ else {
+ this.type="WINDOW";
+ }
+ this.use_gebi = false;
+ this.use_css = false;
+ this.use_layers = false;
+ if (document.getElementById) { this.use_gebi = true; }
+ else if (document.all) { this.use_css = true; }
+ else if (document.layers) { this.use_layers = true; }
+ else { this.type = "WINDOW"; }
+ this.offsetX = 0;
+ this.offsetY = 0;
+ // Method mappings
+ this.getXYPosition = PopupWindow_getXYPosition;
+ this.populate = PopupWindow_populate;
+ this.setUrl = PopupWindow_setUrl;
+ this.setWindowProperties = PopupWindow_setWindowProperties;
+ this.refresh = PopupWindow_refresh;
+ this.showPopup = PopupWindow_showPopup;
+ this.hidePopup = PopupWindow_hidePopup;
+ this.setSize = PopupWindow_setSize;
+ this.isClicked = PopupWindow_isClicked;
+ this.autoHide = PopupWindow_autoHide;
+ this.hideIfNotClicked = PopupWindow_hideIfNotClicked;
+ }
+
+function CalendarPopup() {
+ var c;
+ if (arguments.length>0) {
+ c = new PopupWindow(arguments[0]);
+ }
+ else {
+ c = new PopupWindow();
+ c.setSize(150,175);
+ }
+ c.offsetX = -152;
+ c.offsetY = 25;
+ c.autoHide();
+ // Calendar-specific properties
+ c.monthNames = new Array("January","February","March","April","May","June","July","August","September","October","November","December");
+ c.monthAbbreviations = new Array("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
+ c.dayHeaders = new Array("S","M","T","W","T","F","S");
+ c.returnFunction = "CP_tmpReturnFunction";
+ c.returnMonthFunction = "CP_tmpReturnMonthFunction";
+ c.returnQuarterFunction = "CP_tmpReturnQuarterFunction";
+ c.returnYearFunction = "CP_tmpReturnYearFunction";
+ c.weekStartDay = 0;
+ c.isShowYearNavigation = false;
+ c.displayType = "date";
+ c.disabledWeekDays = new Object();
+ c.disabledDatesExpression = "";
+ c.yearSelectStartOffset = 2;
+ c.currentDate = null;
+ c.todayText="Today";
+ c.cssPrefix="";
+ c.isShowNavigationDropdowns=false;
+ c.isShowYearNavigationInput=false;
+ window.CP_calendarObject = null;
+ window.CP_targetInput = null;
+ window.CP_dateFormat = "MM/dd/yyyy";
+ // Method mappings
+ c.copyMonthNamesToWindow = CP_copyMonthNamesToWindow;
+ c.setReturnFunction = CP_setReturnFunction;
+ c.setReturnMonthFunction = CP_setReturnMonthFunction;
+ c.setReturnQuarterFunction = CP_setReturnQuarterFunction;
+ c.setReturnYearFunction = CP_setReturnYearFunction;
+ c.setMonthNames = CP_setMonthNames;
+ c.setMonthAbbreviations = CP_setMonthAbbreviations;
+ c.setDayHeaders = CP_setDayHeaders;
+ c.setWeekStartDay = CP_setWeekStartDay;
+ c.setDisplayType = CP_setDisplayType;
+ c.setDisabledWeekDays = CP_setDisabledWeekDays;
+ c.addDisabledDates = CP_addDisabledDates;
+ c.setYearSelectStartOffset = CP_setYearSelectStartOffset;
+ c.setTodayText = CP_setTodayText;
+ c.showYearNavigation = CP_showYearNavigation;
+ c.showCalendar = CP_showCalendar;
+ c.hideCalendar = CP_hideCalendar;
+ c.getStyles = getCalendarStyles;
+ c.refreshCalendar = CP_refreshCalendar;
+ c.getCalendar = CP_getCalendar;
+ c.select = CP_select;
+ c.setCssPrefix = CP_setCssPrefix;
+ c.showNavigationDropdowns = CP_showNavigationDropdowns;
+ c.showYearNavigationInput = CP_showYearNavigationInput;
+ c.copyMonthNamesToWindow();
+ // Return the object
+ return c;
+ }
+function CP_copyMonthNamesToWindow() {
+ // Copy these values over to the date.js
+ if (typeof(window.MONTH_NAMES)!="undefined" && window.MONTH_NAMES!=null) {
+ window.MONTH_NAMES = new Array();
+ for (var i=0; i<this.monthNames.length; i++) {
+ window.MONTH_NAMES[window.MONTH_NAMES.length] = this.monthNames[i];
+ }
+ for (var i=0; i<this.monthAbbreviations.length; i++) {
+ window.MONTH_NAMES[window.MONTH_NAMES.length] = this.monthAbbreviations[i];
+ }
+ }
+}
+// Temporary default functions to be called when items clicked, so no error is thrown
+function CP_tmpReturnFunction(y,m,d) {
+ if (window.CP_targetInput!=null) {
+ var dt = new Date(y,m-1,d,0,0,0);
+ if (window.CP_calendarObject!=null) { window.CP_calendarObject.copyMonthNamesToWindow(); }
+ window.CP_targetInput.value = formatDate(dt,window.CP_dateFormat);
+ }
+ else {
+ alert('Use setReturnFunction() to define which function will get the clicked results!');
+ }
+ }
+function CP_tmpReturnMonthFunction(y,m) {
+ alert('Use setReturnMonthFunction() to define which function will get the clicked results!\nYou clicked: year='+y+' , month='+m);
+ }
+function CP_tmpReturnQuarterFunction(y,q) {
+ alert('Use setReturnQuarterFunction() to define which function will get the clicked results!\nYou clicked: year='+y+' , quarter='+q);
+ }
+function CP_tmpReturnYearFunction(y) {
+ alert('Use setReturnYearFunction() to define which function will get the clicked results!\nYou clicked: year='+y);
+ }
+
+// Set the name of the functions to call to get the clicked item
+function CP_setReturnFunction(name) { this.returnFunction = name; }
+function CP_setReturnMonthFunction(name) { this.returnMonthFunction = name; }
+function CP_setReturnQuarterFunction(name) { this.returnQuarterFunction = name; }
+function CP_setReturnYearFunction(name) { this.returnYearFunction = name; }
+
+// Over-ride the built-in month names
+function CP_setMonthNames() {
+ for (var i=0; i<arguments.length; i++) { this.monthNames[i] = arguments[i]; }
+ this.copyMonthNamesToWindow();
+ }
+
+// Over-ride the built-in month abbreviations
+function CP_setMonthAbbreviations() {
+ for (var i=0; i<arguments.length; i++) { this.monthAbbreviations[i] = arguments[i]; }
+ this.copyMonthNamesToWindow();
+ }
+
+// Over-ride the built-in column headers for each day
+function CP_setDayHeaders() {
+ for (var i=0; i<arguments.length; i++) { this.dayHeaders[i] = arguments[i]; }
+ }
+
+// Set the day of the week (0-7) that the calendar display starts on
+// This is for countries other than the US whose calendar displays start on Monday(1), for example
+function CP_setWeekStartDay(day) { this.weekStartDay = day; }
+
+// Show next/last year navigation links
+function CP_showYearNavigation() { this.isShowYearNavigation = (arguments.length>0)?arguments[0]:true; }
+
+// Which type of calendar to display
+function CP_setDisplayType(type) {
+ if (type!="date"&&type!="week-end"&&type!="month"&&type!="quarter"&&type!="year") { alert("Invalid display type! Must be one of: date,week-end,month,quarter,year"); return false; }
+ this.displayType=type;
+ }
+
+// How many years back to start by default for year display
+function CP_setYearSelectStartOffset(num) { this.yearSelectStartOffset=num; }
+
+// Set which weekdays should not be clickable
+function CP_setDisabledWeekDays() {
+ this.disabledWeekDays = new Object();
+ for (var i=0; i<arguments.length; i++) { this.disabledWeekDays[arguments[i]] = true; }
+ }
+
+// Disable individual dates or ranges
+// Builds an internal logical test which is run via eval() for efficiency
+function CP_addDisabledDates(start, end) {
+ if (arguments.length==1) { end=start; }
+ if (start==null && end==null) { return; }
+ if (this.disabledDatesExpression!="") { this.disabledDatesExpression+= "||"; }
+ if (start!=null) { start = parseDate(start); start=""+start.getFullYear()+LZ(start.getMonth()+1)+LZ(start.getDate());}
+ if (end!=null) { end=parseDate(end); end=""+end.getFullYear()+LZ(end.getMonth()+1)+LZ(end.getDate());}
+ if (start==null) { this.disabledDatesExpression+="(ds<="+end+")"; }
+ else if (end ==null) { this.disabledDatesExpression+="(ds>="+start+")"; }
+ else { this.disabledDatesExpression+="(ds>="+start+"&&ds<="+end+")"; }
+ }
+
+// Set the text to use for the "Today" link
+function CP_setTodayText(text) {
+ this.todayText = text;
+ }
+
+// Set the prefix to be added to all CSS classes when writing output
+function CP_setCssPrefix(val) {
+ this.cssPrefix = val;
+ }
+
+// Show the navigation as an dropdowns that can be manually changed
+function CP_showNavigationDropdowns() { this.isShowNavigationDropdowns = (arguments.length>0)?arguments[0]:true; }
+
+// Show the year navigation as an input box that can be manually changed
+function CP_showYearNavigationInput() { this.isShowYearNavigationInput = (arguments.length>0)?arguments[0]:true; }
+
+// Hide a calendar object
+function CP_hideCalendar() {
+ if (arguments.length > 0) { window.popupWindowObjects[arguments[0]].hidePopup(); }
+ else { this.hidePopup(); }
+ }
+
+// Refresh the contents of the calendar display
+function CP_refreshCalendar(index) {
+ var calObject = window.popupWindowObjects[index];
+ if (arguments.length>1) {
+ calObject.populate(calObject.getCalendar(arguments[1],arguments[2],arguments[3],arguments[4],arguments[5]));
+ }
+ else {
+ calObject.populate(calObject.getCalendar());
+ }
+ calObject.refresh();
+ }
+
+// Populate the calendar and display it
+function CP_showCalendar(anchorname) {
+ if (arguments.length>1) {
+ if (arguments[1]==null||arguments[1]=="") {
+ this.currentDate=new Date();
+ }
+ else {
+ this.currentDate=new Date(parseDate(arguments[1]));
+ }
+ }
+ this.populate(this.getCalendar());
+ this.showPopup(anchorname);
+ }
+
+// Simple method to interface popup calendar with a text-entry box
+function CP_select(inputobj, linkname, format) {
+ var selectedDate=(arguments.length>3)?arguments[3]:null;
+ if (!window.getDateFromFormat) {
+ alert("calendar.select: To use this method you must also include 'date.js' for date formatting");
+ return;
+ }
+ if (this.displayType!="date"&&this.displayType!="week-end") {
+ alert("calendar.select: This function can only be used with displayType 'date' or 'week-end'");
+ return;
+ }
+ if (inputobj.type!="text" && inputobj.type!="hidden" && inputobj.type!="textarea") {
+ alert("calendar.select: Input object passed is not a valid form input object");
+ window.CP_targetInput=null;
+ return;
+ }
+ if (inputobj.disabled) { return; } // Can't use calendar input on disabled form input!
+ window.CP_targetInput = inputobj;
+ window.CP_calendarObject = this;
+ this.currentDate=null;
+ var time=0;
+ if (selectedDate!=null) {
+ time = getDateFromFormat(selectedDate,format)
+ }
+ else if (inputobj.value!="") {
+ time = getDateFromFormat(inputobj.value,format);
+ }
+ if (selectedDate!=null || inputobj.value!="") {
+ if (time==0) { this.currentDate=null; }
+ else { this.currentDate=new Date(time); }
+ }
+ window.CP_dateFormat = format;
+ this.showCalendar(linkname);
+ }
+
+// Get style block needed to display the calendar correctly
+function getCalendarStyles() {
+ var result = "";
+ var p = "";
+ if (this!=null && typeof(this.cssPrefix)!="undefined" && this.cssPrefix!=null && this.cssPrefix!="") { p=this.cssPrefix; }
+ result += "<STYLE>\n";
+ result += "."+p+"cpYearNavigation,."+p+"cpMonthNavigation { background-color:#C0C0C0; text-align:center; vertical-align:center; text-decoration:none; color:#000000; font-weight:bold; }\n";
+ result += "."+p+"cpDayColumnHeader, ."+p+"cpYearNavigation,."+p+"cpMonthNavigation,."+p+"cpCurrentMonthDate,."+p+"cpCurrentMonthDateDisabled,."+p+"cpOtherMonthDate,."+p+"cpOtherMonthDateDisabled,."+p+"cpCurrentDate,."+p+"cpCurrentDateDisabled,."+p+"cpTodayText,."+p+"cpTodayTextDisabled,."+p+"cpText { font-family:arial; font-size:8pt; }\n";
+ result += "TD."+p+"cpDayColumnHeader { text-align:right; border:solid thin #C0C0C0;border-width:0px 0px 1px 0px; }\n";
+ result += "."+p+"cpCurrentMonthDate, ."+p+"cpOtherMonthDate, ."+p+"cpCurrentDate { text-align:right; text-decoration:none; }\n";
+ result += "."+p+"cpCurrentMonthDateDisabled, ."+p+"cpOtherMonthDateDisabled, ."+p+"cpCurrentDateDisabled { color:#D0D0D0; text-align:right; text-decoration:line-through; }\n";
+ result += "."+p+"cpCurrentMonthDate, .cpCurrentDate { color:#000000; }\n";
+ result += "."+p+"cpOtherMonthDate { color:#808080; }\n";
+ result += "TD."+p+"cpCurrentDate { color:white; background-color: #C0C0C0; border-width:1px; border:solid thin #800000; }\n";
+ result += "TD."+p+"cpCurrentDateDisabled { border-width:1px; border:solid thin #FFAAAA; }\n";
+ result += "TD."+p+"cpTodayText, TD."+p+"cpTodayTextDisabled { border:solid thin #C0C0C0; border-width:1px 0px 0px 0px;}\n";
+ result += "A."+p+"cpTodayText, SPAN."+p+"cpTodayTextDisabled { height:20px; }\n";
+ result += "A."+p+"cpTodayText { color:black; }\n";
+ result += "."+p+"cpTodayTextDisabled { color:#D0D0D0; }\n";
+ result += "."+p+"cpBorder { border:solid thin #808080; }\n";
+ result += "</STYLE>\n";
+ return result;
+ }
+
+// Return a string containing all the calendar code to be displayed
+function CP_getCalendar() {
+ var now = new Date();
+ // Reference to window
+ if (this.type == "WINDOW") { var windowref = "window.opener."; }
+ else { var windowref = ""; }
+ var result = "";
+ // If POPUP, write entire HTML document
+ if (this.type == "WINDOW") {
+ result += "<HTML><HEAD><TITLE>Calendar</TITLE>"+this.getStyles()+"</HEAD><BODY MARGINWIDTH=0 MARGINHEIGHT=0 TOPMARGIN=0 RIGHTMARGIN=0 LEFTMARGIN=0>\n";
+ result += '<CENTER><TABLE WIDTH=100% BORDER=0 BORDERWIDTH=0 CELLSPACING=0 CELLPADDING=0>\n';
+ }
+ else {
+ result += '<TABLE CLASS="'+this.cssPrefix+'cpBorder" WIDTH=144 BORDER=1 BORDERWIDTH=1 CELLSPACING=0 CELLPADDING=1>\n';
+ result += '<TR><TD ALIGN=CENTER>\n';
+ result += '<CENTER>\n';
+ }
+ // Code for DATE display (default)
+ // -------------------------------
+ if (this.displayType=="date" || this.displayType=="week-end") {
+ if (this.currentDate==null) { this.currentDate = now; }
+ if (arguments.length > 0) { var month = arguments[0]; }
+ else { var month = this.currentDate.getMonth()+1; }
+ if (arguments.length > 1 && arguments[1]>0 && arguments[1]-0==arguments[1]) { var year = arguments[1]; }
+ else { var year = this.currentDate.getFullYear(); }
+ var daysinmonth= new Array(0,31,28,31,30,31,30,31,31,30,31,30,31);
+ if ( ( (year%4 == 0)&&(year%100 != 0) ) || (year%400 == 0) ) {
+ daysinmonth[2] = 29;
+ }
+ var current_month = new Date(year,month-1,1);
+ var display_year = year;
+ var display_month = month;
+ var display_date = 1;
+ var weekday= current_month.getDay();
+ var offset = 0;
+
+ offset = (weekday >= this.weekStartDay) ? weekday-this.weekStartDay : 7-this.weekStartDay+weekday ;
+ if (offset > 0) {
+ display_month--;
+ if (display_month < 1) { display_month = 12; display_year--; }
+ display_date = daysinmonth[display_month]-offset+1;
+ }
+ var next_month = month+1;
+ var next_month_year = year;
+ if (next_month > 12) { next_month=1; next_month_year++; }
+ var last_month = month-1;
+ var last_month_year = year;
+ if (last_month < 1) { last_month=12; last_month_year--; }
+ var date_class;
+ if (this.type!="WINDOW") {
+ result += "<TABLE WIDTH=144 BORDER=0 BORDERWIDTH=0 CELLSPACING=0 CELLPADDING=0>";
+ }
+ result += '<TR>\n';
+ var refresh = windowref+'CP_refreshCalendar';
+ var refreshLink = 'javascript:' + refresh;
+ if (this.isShowNavigationDropdowns) {
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="78" COLSPAN="3"><select CLASS="'+this.cssPrefix+'cpMonthNavigation" name="cpMonth" onChange="'+refresh+'('+this.index+',this.options[this.selectedIndex].value-0,'+(year-0)+');">';
+ for( var monthCounter=1; monthCounter<=12; monthCounter++ ) {
+ var selected = (monthCounter==month) ? 'SELECTED' : '';
+ result += '<option value="'+monthCounter+'" '+selected+'>'+this.monthNames[monthCounter-1]+'</option>';
+ }
+ result += '</select></TD>';
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="10">&nbsp;</TD>';
+
+ result += '<TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="56" COLSPAN="3"><select CLASS="'+this.cssPrefix+'cpYearNavigation" name="cpYear" onChange="'+refresh+'('+this.index+','+month+',this.options[this.selectedIndex].value-0);">';
+ for( var yearCounter=year-this.yearSelectStartOffset; yearCounter<=year+this.yearSelectStartOffset; yearCounter++ ) {
+ var selected = (yearCounter==year) ? 'SELECTED' : '';
+ result += '<option value="'+yearCounter+'" '+selected+'>'+yearCounter+'</option>';
+ }
+ result += '</select></TD>';
+ }
+ else {
+ if (this.isShowYearNavigation) {
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="10"><A CLASS="'+this.cssPrefix+'cpMonthNavigation" HREF="'+refreshLink+'('+this.index+','+last_month+','+last_month_year+');">&lt;</A></TD>';
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="58"><SPAN CLASS="'+this.cssPrefix+'cpMonthNavigation">'+this.monthNames[month-1]+'</SPAN></TD>';
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="10"><A CLASS="'+this.cssPrefix+'cpMonthNavigation" HREF="'+refreshLink+'('+this.index+','+next_month+','+next_month_year+');">&gt;</A></TD>';
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="10">&nbsp;</TD>';
+
+ result += '<TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="10"><A CLASS="'+this.cssPrefix+'cpYearNavigation" HREF="'+refreshLink+'('+this.index+','+month+','+(year-1)+');">&lt;</A></TD>';
+ if (this.isShowYearNavigationInput) {
+ result += '<TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="36"><INPUT NAME="cpYear" CLASS="'+this.cssPrefix+'cpYearNavigation" SIZE="4" MAXLENGTH="4" VALUE="'+year+'" onBlur="'+refresh+'('+this.index+','+month+',this.value-0);"></TD>';
+ }
+ else {
+ result += '<TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="36"><SPAN CLASS="'+this.cssPrefix+'cpYearNavigation">'+year+'</SPAN></TD>';
+ }
+ result += '<TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="10"><A CLASS="'+this.cssPrefix+'cpYearNavigation" HREF="'+refreshLink+'('+this.index+','+month+','+(year+1)+');">&gt;</A></TD>';
+ }
+ else {
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="22"><A CLASS="'+this.cssPrefix+'cpMonthNavigation" HREF="'+refreshLink+'('+this.index+','+last_month+','+last_month_year+');">&lt;&lt;</A></TD>\n';
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="100"><SPAN CLASS="'+this.cssPrefix+'cpMonthNavigation">'+this.monthNames[month-1]+' '+year+'</SPAN></TD>\n';
+ result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="22"><A CLASS="'+this.cssPrefix+'cpMonthNavigation" HREF="'+refreshLink+'('+this.index+','+next_month+','+next_month_year+');">&gt;&gt;</A></TD>\n';
+ }
+ }
+ result += '</TR></TABLE>\n';
+ result += '<TABLE WIDTH=120 BORDER=0 CELLSPACING=0 CELLPADDING=1 ALIGN=CENTER>\n';
+ result += '<TR>\n';
+ for (var j=0; j<7; j++) {
+
+ result += '<TD CLASS="'+this.cssPrefix+'cpDayColumnHeader" WIDTH="14%"><SPAN CLASS="'+this.cssPrefix+'cpDayColumnHeader">'+this.dayHeaders[(this.weekStartDay+j)%7]+'</TD>\n';
+ }
+ result += '</TR>\n';
+ for (var row=1; row<=6; row++) {
+ result += '<TR>\n';
+ for (var col=1; col<=7; col++) {
+ var disabled=false;
+ if (this.disabledDatesExpression!="") {
+ var ds=""+display_year+LZ(display_month)+LZ(display_date);
+ eval("disabled=("+this.disabledDatesExpression+")");
+ }
+ var dateClass = "";
+ if ((display_month == this.currentDate.getMonth()+1) && (display_date==this.currentDate.getDate()) && (display_year==this.currentDate.getFullYear())) {
+ dateClass = "cpCurrentDate";
+ }
+ else if (display_month == month) {
+ dateClass = "cpCurrentMonthDate";
+ }
+ else {
+ dateClass = "cpOtherMonthDate";
+ }
+ if (disabled || this.disabledWeekDays[col-1]) {
+ result += ' <TD CLASS="'+this.cssPrefix+dateClass+'"><SPAN CLASS="'+this.cssPrefix+dateClass+'Disabled">'+display_date+'</SPAN></TD>\n';
+ }
+ else {
+ var selected_date = display_date;
+ var selected_month = display_month;
+ var selected_year = display_year;
+ if (this.displayType=="week-end") {
+ var d = new Date(selected_year,selected_month-1,selected_date,0,0,0,0);
+ d.setDate(d.getDate() + (7-col));
+ selected_year = d.getYear();
+ if (selected_year < 1000) { selected_year += 1900; }
+ selected_month = d.getMonth()+1;
+ selected_date = d.getDate();
+ }
+ result += ' <TD CLASS="'+this.cssPrefix+dateClass+'"><A HREF="javascript:'+windowref+this.returnFunction+'('+selected_year+','+selected_month+','+selected_date+');'+windowref+'CP_hideCalendar(\''+this.index+'\');" CLASS="'+this.cssPrefix+dateClass+'">'+display_date+'</A></TD>\n';
+ }
+ display_date++;
+ if (display_date > daysinmonth[display_month]) {
+ display_date=1;
+ display_month++;
+ }
+ if (display_month > 12) {
+ display_month=1;
+ display_year++;
+ }
+ }
+ result += '</TR>';
+ }
+ var current_weekday = now.getDay() - this.weekStartDay;
+ if (current_weekday < 0) {
+ current_weekday += 7;
+ }
+ result += '<TR>\n';
+ result += ' <TD COLSPAN=7 ALIGN=CENTER CLASS="'+this.cssPrefix+'cpTodayText">\n';
+ if (this.disabledDatesExpression!="") {
+ var ds=""+now.getFullYear()+LZ(now.getMonth()+1)+LZ(now.getDate());
+ eval("disabled=("+this.disabledDatesExpression+")");
+ }
+ if (disabled || this.disabledWeekDays[current_weekday+1]) {
+ result += ' <SPAN CLASS="'+this.cssPrefix+'cpTodayTextDisabled">'+this.todayText+'</SPAN>\n';
+ }
+ else {
+ result += ' <A CLASS="'+this.cssPrefix+'cpTodayText" HREF="javascript:'+windowref+this.returnFunction+'(\''+now.getFullYear()+'\',\''+(now.getMonth()+1)+'\',\''+now.getDate()+'\');'+windowref+'CP_hideCalendar(\''+this.index+'\');">'+this.todayText+'</A>\n';
+ }
+ result += ' <BR>\n';
+ result += ' </TD></TR></TABLE></CENTER></TD></TR></TABLE>\n';
+ }
+
+ // Code common for MONTH, QUARTER, YEAR
+ // ------------------------------------
+ if (this.displayType=="month" || this.displayType=="quarter" || this.displayType=="year") {
+ if (arguments.length > 0) { var year = arguments[0]; }
+ else {
+ if (this.displayType=="year") { var year = now.getFullYear()-this.yearSelectStartOffset; }
+ else { var year = now.getFullYear(); }
+ }
+ if (this.displayType!="year" && this.isShowYearNavigation) {
+ result += "<TABLE WIDTH=144 BORDER=0 BORDERWIDTH=0 CELLSPACING=0 CELLPADDING=0>";
+ result += '<TR>\n';
+ result += ' <TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="22"><A CLASS="'+this.cssPrefix+'cpYearNavigation" HREF="javascript:'+windowref+'CP_refreshCalendar('+this.index+','+(year-1)+');">&lt;&lt;</A></TD>\n';
+ result += ' <TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="100">'+year+'</TD>\n';
+ result += ' <TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="22"><A CLASS="'+this.cssPrefix+'cpYearNavigation" HREF="javascript:'+windowref+'CP_refreshCalendar('+this.index+','+(year+1)+');">&gt;&gt;</A></TD>\n';
+ result += '</TR></TABLE>\n';
+ }
+ }
+
+ // Code for MONTH display
+ // ----------------------
+ if (this.displayType=="month") {
+ // If POPUP, write entire HTML document
+ result += '<TABLE WIDTH=120 BORDER=0 CELLSPACING=1 CELLPADDING=0 ALIGN=CENTER>\n';
+ for (var i=0; i<4; i++) {
+ result += '<TR>';
+ for (var j=0; j<3; j++) {
+ var monthindex = ((i*3)+j);
+ result += '<TD WIDTH=33% ALIGN=CENTER><A CLASS="'+this.cssPrefix+'cpText" HREF="javascript:'+windowref+this.returnMonthFunction+'('+year+','+(monthindex+1)+');'+windowref+'CP_hideCalendar(\''+this.index+'\');" CLASS="'+date_class+'">'+this.monthAbbreviations[monthindex]+'</A></TD>';
+ }
+ result += '</TR>';
+ }
+ result += '</TABLE></CENTER></TD></TR></TABLE>\n';
+ }
+
+ // Code for QUARTER display
+ // ------------------------
+ if (this.displayType=="quarter") {
+ result += '<BR><TABLE WIDTH=120 BORDER=1 CELLSPACING=0 CELLPADDING=0 ALIGN=CENTER>\n';
+ for (var i=0; i<2; i++) {
+ result += '<TR>';
+ for (var j=0; j<2; j++) {
+ var quarter = ((i*2)+j+1);
+ result += '<TD WIDTH=50% ALIGN=CENTER><BR><A CLASS="'+this.cssPrefix+'cpText" HREF="javascript:'+windowref+this.returnQuarterFunction+'('+year+','+quarter+');'+windowref+'CP_hideCalendar(\''+this.index+'\');" CLASS="'+date_class+'">Q'+quarter+'</A><BR><BR></TD>';
+ }
+ result += '</TR>';
+ }
+ result += '</TABLE></CENTER></TD></TR></TABLE>\n';
+ }
+
+ // Code for YEAR display
+ // ---------------------
+ if (this.displayType=="year") {
+ var yearColumnSize = 4;
+ result += "<TABLE WIDTH=144 BORDER=0 BORDERWIDTH=0 CELLSPACING=0 CELLPADDING=0>";
+ result += '<TR>\n';
+ result += ' <TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="50%"><A CLASS="'+this.cssPrefix+'cpYearNavigation" HREF="javascript:'+windowref+'CP_refreshCalendar('+this.index+','+(year-(yearColumnSize*2))+');">&lt;&lt;</A></TD>\n';
+ result += ' <TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="50%"><A CLASS="'+this.cssPrefix+'cpYearNavigation" HREF="javascript:'+windowref+'CP_refreshCalendar('+this.index+','+(year+(yearColumnSize*2))+');">&gt;&gt;</A></TD>\n';
+ result += '</TR></TABLE>\n';
+ result += '<TABLE WIDTH=120 BORDER=0 CELLSPACING=1 CELLPADDING=0 ALIGN=CENTER>\n';
+ for (var i=0; i<yearColumnSize; i++) {
+ for (var j=0; j<2; j++) {
+ var currentyear = year+(j*yearColumnSize)+i;
+ result += '<TD WIDTH=50% ALIGN=CENTER><A CLASS="'+this.cssPrefix+'cpText" HREF="javascript:'+windowref+this.returnYearFunction+'('+currentyear+');'+windowref+'CP_hideCalendar(\''+this.index+'\');" CLASS="'+date_class+'">'+currentyear+'</A></TD>';
+ }
+ result += '</TR>';
+ }
+ result += '</TABLE></CENTER></TD></TR></TABLE>\n';
+ }
+ // Common
+ if (this.type == "WINDOW") {
+ result += "</BODY></HTML>\n";
+ }
+ return result;
+ }
+
+
+var cal1x = new CalendarPopup("caldiv1x");
+var cal2x = new CalendarPopup("caldiv2x");
diff --git a/exilog_parse.pm b/exilog_parse.pm
new file mode 100644
index 0000000..2074b13
--- /dev/null
+++ b/exilog_parse.pm
@@ -0,0 +1,351 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_parse;
+use strict;
+use exilog_util;
+use Digest::MD5 qw( md5_base64 );
+
+use Data::Dumper;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ &parse_message_line
+ &parse_reject_line
+ &date_to_stamp
+ &stamp_to_date
+ );
+
+ %EXPORT_TAGS = ();
+ @EXPORT_OK = qw();
+}
+
+sub _parse_error {
+ my $subj = shift || "";
+ my $h = shift || {};
+
+ $subj = _parse_delivery($subj,$h);
+
+ m/()()/;
+ if ($subj =~ / host ([^ ]+?) \[([0-9.]+?)\]\:/) {
+ $h->{host_addr} = $2;
+ $h->{host_dns} = $1;
+ };
+ $subj =~ s/^[ :]+//;
+ $subj =~ s/ +$//;
+ $h->{errmsg} = $subj if ($subj);
+
+ return $subj;
+};
+
+
+sub _parse_deferral {
+ my $subj = shift || "";
+ my $h = shift || {};
+
+ $subj = _parse_delivery($subj,$h);
+
+ if ($subj =~ / host ([^ ]+?) \[([0-9.]+?)\]\:/) {
+ $h->{host_addr} = $2;
+ $h->{host_dns} = $1;
+ };
+ $subj =~ s/^[ :]+//;
+ $subj =~ s/ +$//;
+ $h->{errmsg} = $subj if ($subj);
+
+ return $subj;
+};
+
+
+sub _parse_delivery {
+ my $subj = shift || "";
+ my $h = shift || {};
+
+
+ # When +sender_on_delivery is set, cut away the F=<> part
+ $subj =~ s/[PF]\=[^ ]+ //;
+
+ m/()/;
+
+ $subj =~ s/^.+?[\=\-\*][\>\=\*] (.+?)((\: )|( R\=)|( \<)|( \())/$2/;
+ $h->{rcpt_final} = $1 if ($1);
+ $subj =~ s/^\: //;
+ $subj =~ s/^ +//;
+
+ m/()/;
+ $subj =~ s/^\((.+?)\) //;
+ $h->{rcpt_intermediate} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/^\<(.+?)\> //;
+ if ($1) {
+ $h->{rcpt} = $1;
+ }
+ else {
+ $h->{rcpt} = $h->{rcpt_final};
+ };
+
+ m/()/;
+ $subj =~ s/R\=([^ \:]+)//;
+ $h->{router} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/ST\=([^ \:]+)//;
+ $h->{shadow_transport} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/T\=([^ \:]+)//;
+ $h->{transport} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/X\=([^ ]+)//;
+ $h->{tls_cipher} = $1 if ($1);
+
+ m/()()/;
+ $subj =~ s/H\=([^ ]+) \[(.+?)\]//;
+ $h->{host_dns} = $1 if ($1);
+ $h->{host_addr} = $2 if ($2);
+
+ return $subj;
+};
+
+
+
+
+sub _parse_arrival {
+ my $subj = shift || "";
+ my $h = shift || {};
+
+ m/()/;
+ $subj =~ s/^.+?\<\= (.+?) //;
+ $h->{mailfrom} = $1 if ($1);
+
+ m/()()/;
+ $subj =~ s/H\=(.+?) ([A-Za-z]\=)/$2/;
+ if ($1) {
+ my $hstr = $1;
+ m/()/;
+ $hstr =~ s/\[([0-9.]+)\]$//;
+ $h->{host_addr} = $1 if ($1);
+
+ $hstr =~ s/^ +//;
+ $hstr =~ s/ +$//;
+
+ m/()/;
+ $hstr =~ s/\((.+?)\)$//;
+ $h->{host_helo} = $1 if ($1);
+
+ $hstr =~ s/^ +//;
+ $hstr =~ s/ +$//;
+
+ # if we have something left over now, it must
+ # be a confirmed rdns host name
+ $h->{host_rdns} = $hstr if ($hstr);
+ }
+
+ m/()/;
+ $subj =~ s/P\=([^ ]+)//;
+ $h->{proto} = $1 if ($1);
+ if ($1 =~ /^local/) {
+ # U= contains local user account
+ m/()/;
+ $subj =~ s/U\=([^ ]+)//;
+ $h->{user} = $1 if ($1);
+ }
+ elsif ( ($1 eq 'asmtp') || ($1 eq 'esmtpa') || ($1 eq 'esmtpsa') ) {
+ # fill in both auth user and ident
+ m/()/;
+ $subj =~ s/A\=([^ ]+)//;
+ $h->{user} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/U\=([^ ]+)//;
+ $h->{host_ident} = $1 if ($1);
+ }
+ else {
+ # U= contains remote ident
+ m/()/;
+ $subj =~ s/U\=([^ ]+)//;
+ $h->{host_ident} = $1 if ($1);
+ };
+
+ m/()/;
+ $subj =~ s/S\=([^ ]+)//;
+ $h->{size} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/id\=([^ ]+)//;
+ $h->{msgid} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/X\=([^ ]+)//;
+ $h->{tls_cipher} = $1 if ($1);
+
+ m/()/;
+ $subj =~ s/R\=([^ ]+)//;
+ $h->{bounce_parent} = $1 if ($1);
+
+ return $subj;
+};
+
+sub _parse_reject {
+ my $subj = shift;
+ my $h = shift;
+
+ m/()()/;
+ $subj =~ s/H\=(.+?) \[(.+?)\] //;
+ if ($1 && $2) {
+ $h->{host_addr} = $2;
+ my $hstr = $1;
+
+ $hstr =~ s/^ +//;
+ $hstr =~ s/ +$//;
+
+ m/()/;
+ $hstr =~ s/\((.+?)\)$//;
+ $h->{host_helo} = $1 if ($1);
+
+ $hstr =~ s/^ +//;
+ $hstr =~ s/ +$//;
+
+ # if we have something left over now, it must
+ # be a confirmed rdns host name
+ $h->{host_rdns} = $hstr if ($hstr);
+ };
+
+ m/()/;
+ $subj =~ s/U\=(.+?) //;
+ $h->{host_ident} = $1 if ($1);
+
+ m/()()/;
+ $subj =~ s/F\=(\<.*?\>) //;
+ $h->{mailfrom} = $1 if ($1);
+ if (exists($h->{mailfrom})) {
+ unless ($h->{mailfrom} eq '<>') {
+ $h->{mailfrom} =~ s/[<>]//g;
+ }
+ };
+
+ m/()()/;
+ $subj =~ m/\<(.+?)\>/;
+ if ($1) {
+ $h->{rcpt} = $1;
+ };
+
+ return $subj;
+};
+
+
+# Parse a reject line
+sub parse_reject_line {
+ my $subj = shift || "";
+ chomp($subj);
+
+ my $h = { 'table' => 'rejects' };
+
+ # There are 2 types of rejects: one without a message ID (pre-DATA)
+ # and one with message ID (post-DATA). Try the latter first.
+
+ m/()()()()/;
+ $subj =~ m/(\d{4}-\d\d-\d\d) (\d\d:\d\d:\d\d( [-+]\d{4})?) ([A-Za-z0-9]{6}-[A-Za-z0-9]{6}-[A-Za-z0-9]{2}) (H=.*)$/;
+ my ($date,$tod,$msgid,$line) = ($1,$2,$4,$5);
+ if ($date && $tod && $msgid && $line) {
+ # line with message id
+ $h->{data}->{message_id} = $msgid;
+ }
+ else {
+ # try format without message id
+ m/()()()()/;
+ $subj =~ m/(\d{4}-\d\d-\d\d) (\d\d:\d\d:\d\d( [-+]\d{4})?) (H=.*)$/;
+ ($date,$tod,$line) = ($1,$2,$4);
+ unless ($date && $tod && $line) {
+ # unparsable
+ return 0;
+ };
+ # Add custom "Message ID" hash
+ $h->{data}->{message_id} = substr(md5_base64($date,$tod,$line),0,16);
+ };
+
+ $h->{data}->{timestamp} = date_to_stamp($date,$tod);
+ $h->{data}->{errmsg} = substr(_parse_reject($line,$h->{data}),0,255);
+
+ return $h;
+};
+
+
+# Parse line that relates to an actual message.
+sub parse_message_line {
+ my $subj = shift || "";
+ chomp($subj);
+
+ # Exception: do not use "retry time not reached [for any host]".
+ # It's just too spammy and gets logged by default.
+ return 0 if ($subj =~ /retry time not reached$/);
+ return 0 if ($subj =~ /retry time not reached for any host$/);
+
+ # Grab date, time and message id
+ $subj =~ m/(\d{4}-\d\d-\d\d) (\d\d:\d\d:\d\d( [-+]\d{4})?) ([A-Za-z0-9]{6}-[A-Za-z0-9]{6}-[A-Za-z0-9]{2}) (([^ ]+).*)$/;
+ my ($date,$tod,$msgid,$line,$type) = ($1,$2,$4,$5,$6);
+ $line =~ s/^ +// if (defined($line));
+ unless ($date && $tod && $msgid && $line && $type) {
+ # non-message based line
+ return 0;
+ };
+
+ # removed fttb, too much overhead
+ #my $h = { 'data' => { 'line' => $line, 'message_id' => $msgid } };
+ my $h = { 'data' => { 'message_id' => $msgid } };
+
+
+ if ($type eq '<=') {
+ $h->{table} = 'messages';
+ $h->{data}->{timestamp} = date_to_stamp($date,$tod);
+ _parse_arrival($subj,$h->{data});
+ }
+ elsif (($type eq '=>') || ($type eq '->') || ($type eq '*>')) {
+ $h->{table} = 'deliveries';
+ $h->{data}->{timestamp} = date_to_stamp($date,$tod);
+ _parse_delivery($subj,$h->{data});
+ }
+ elsif ($type eq '**') {
+ $h->{table} = 'errors';
+ $h->{data}->{timestamp} = date_to_stamp($date,$tod);
+ _parse_error($subj,$h->{data});
+ }
+ elsif ($type eq '==') {
+ $h->{table} = 'deferrals';
+ $h->{data}->{timestamp} = date_to_stamp($date,$tod);
+ _parse_deferral($subj,$h->{data});
+ }
+ elsif ($type eq 'Completed') {
+ $h->{table} = 'messages';
+ $h->{data}->{completed} = date_to_stamp($date,$tod);
+ }
+ else {
+ if ($line =~ /^H\=.*rejected/) {
+ # looks like a reject line after DATA, pass on
+ return 0;
+ };
+
+ $h->{table} = 'unknown';
+ $h->{data}->{timestamp} = date_to_stamp($date,$tod);
+ $h->{data}->{line} = substr($line,0,255);
+ };
+
+ return $h;
+};
+
+1;
diff --git a/exilog_sql.pm b/exilog_sql.pm
new file mode 100644
index 0000000..fc8bc71
--- /dev/null
+++ b/exilog_sql.pm
@@ -0,0 +1,556 @@
+#!/usr/bin/perl
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_sql;
+use strict;
+use DBI;
+use exilog_config;
+use exilog_util;
+
+use Data::Dumper;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ # set the version for version checking
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ &reconnect
+ &sql_select
+ &sql_delete
+ &sql_optimize
+ &sql_count
+ &sql_queue_add
+ &sql_queue_update
+ &sql_queue_delete
+ &sql_queue_set_action
+ &sql_queue_clear_action
+ &write_message
+ );
+
+ %EXPORT_TAGS = ();
+
+ # your exported package globals go here,
+ # as well as any optionally exported functions
+ @EXPORT_OK = qw();
+}
+
+
+# open DB connection
+my $dbh = DBI->connect($config->{sql}->{DBI}, $config->{sql}->{user}, $config->{sql}->{pass});
+unless (defined($dbh) && $dbh) {
+ print STDERR "[exilog_sql] Can't open exilog database.\n";
+ exit(255);
+};
+
+sub reconnect {
+ my $conditional = shift || 0;
+ if ($conditional) {
+ return 1 if ($dbh->ping);
+ };
+ eval {
+ $dbh->disconnect() if (defined($dbh));
+ };
+ $dbh = 0;
+ $dbh = DBI->connect($config->{sql}->{DBI}, $config->{sql}->{user}, $config->{sql}->{pass});
+ unless (defined($dbh) && $dbh) {
+ print STDERR "[exilog_sql] Can't open exilog database.\n";
+ return 0;
+ };
+ return 1;
+};
+
+
+# --------------------------------------------------------------------------
+# Generic Stubs, these are just frontends that call the backend-specific
+# SQL subroutines for each database type.
+sub write_message {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_write_message" }(@_);
+};
+
+sub sql_select {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_select" }(@_);
+};
+
+sub sql_delete {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_delete" }(@_);
+};
+
+sub sql_optimize {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_optimize" }(@_);
+};
+
+sub sql_queue_add {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_queue_add" }(@_);
+};
+
+sub sql_queue_update {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_queue_update" }(@_);
+};
+
+sub sql_queue_delete {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_queue_delete" }(@_);
+};
+
+sub sql_queue_set_action {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_queue_set_action" }(@_);
+};
+
+sub sql_queue_clear_action {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_queue_clear_action" }(@_);
+};
+
+sub sql_count {
+ no strict "refs";
+ return &{ "_".$config->{sql}->{type}."_sql_count" }(@_);
+};
+# --------------------------------------------------------------------------
+
+
+# --------------------------------------------------------------------------
+# PostgreSQL functions
+sub _pgsql_sql_count {
+ my $where = shift;
+ my $criteria = shift || {};
+
+ my $sql = "SELECT ".
+ "COUNT(*) ".
+ "FROM ".$where.
+ ((scalar keys %{ $criteria } ) ? " "._build_WHERE($criteria) : "" );
+
+ my $sh = $dbh->prepare($sql);
+ $sh->execute;
+ my $tmp = $sh->fetchrow_arrayref();
+ return @{$tmp}[0];
+};
+
+sub _pgsql_sql_queue_delete {
+ my $spool_path = shift;
+
+ $dbh->do("DELETE FROM queue WHERE spool_path='$spool_path'");
+};
+
+sub _pgsql_sql_queue_update {
+ my $hdr = shift;
+
+ return unless (ref($hdr) eq 'HASH');
+
+ my $server = $hdr->{server};
+ my $message_id = $hdr->{message_id};
+ delete $hdr->{server};
+ delete $hdr->{message_id};
+
+ # PostgreSQL is case sensitive by default. Nice feature,
+ # but it complicates our life tremendously.
+ # Since we want to keep indexes working, the columns in
+ # this list are lowercased before they are inserted. Sigh.
+ my @lowercase = ( 'mailfrom', 'recipients_delivered', 'recipients_pending' );
+ foreach my $col (@lowercase) {
+ $hdr->{$col} = lc($hdr->{$col}) if (edt($hdr,$col));
+ };
+
+ my @tmp;
+ foreach my $item (keys %{ $hdr }) {
+ my $value = $hdr->{$item};
+ $value =~ s/\'/\'\'/g;
+ $value =~ s/\n/\\n/g;
+ push @tmp, $item.'='."'".$value."'";
+ };
+
+ $dbh->do("UPDATE queue SET ".join(",",@tmp)." WHERE message_id='".$message_id."' AND server='".$server."'");
+};
+
+sub _pgsql_sql_queue_add {
+ my $hdr = shift;
+
+ return unless (ref($hdr) eq 'HASH');
+
+ # PostgreSQL is case sensitive by default. Nice feature,
+ # but it complicates our life tremendously.
+ # Since we want to keep indexes working, the columns in
+ # this list are lowercased before they are inserted. Sigh.
+ my @lowercase = ( 'mailfrom', 'recipients_delivered', 'recipients_pending' );
+ foreach my $col (@lowercase) {
+ $hdr->{$col} = lc($hdr->{$col}) if (edt($hdr,$col));
+ };
+
+ my @fields = sort {$a cmp $b} keys(%{$hdr});
+ my @vals = ();
+ foreach (@fields) {
+ my $val = $hdr->{$_};
+ $val =~ s/\'/\'\'/g;
+ $val =~ s/\n/\\n/g;
+ push @vals, "'".$val."'";
+ };
+
+ $dbh->do("INSERT INTO queue (".join(',',@fields).") VALUES(".join(',',@vals).")");
+};
+
+sub _pgsql_sql_optimize {
+ my $where = shift || "nothing";
+
+ my $sql = "OPTIMIZE TABLE ".$where;
+ my $sh = $dbh->prepare($sql);
+ $sh->execute;
+ $sh->finish;
+
+ return 1;
+};
+
+sub _pgsql_sql_delete {
+ my $where = shift || "nothing";
+ my $criteria = shift || {};
+
+ my $sql = "DELETE FROM ".$where.
+ ((scalar keys %{ $criteria } ) ? " "._build_WHERE($criteria) : "" );
+
+ my $sh = $dbh->prepare($sql);
+ my $num = $sh->execute;
+ $sh->finish;
+
+ return (($num eq '0E0') ? 0 : $num);
+};
+
+sub _pgsql_sql_select {
+ my $where = shift;
+ my @what = @{ (shift || [ "*" ]) };
+ my $criteria = shift || {};
+ my $order_by = shift || "";
+ my $order_direction = shift || "DESC";
+ my $limit_min = shift;
+ my $limit_max = shift;
+ my $distinct = shift;
+
+ my $sql = "SELECT ".
+ (defined($distinct) ? "DISTINCT " : "").
+ join(", ", @what).
+ " FROM ".$where.
+ ((scalar keys %{ $criteria } ) ? " "._build_WHERE($criteria) : "" ).
+ ($order_by ? " ORDER BY ".$order_by." ".$order_direction : "").
+ (defined($limit_min) ? " LIMIT ".$limit_min : "").
+ (defined($limit_max) ? ",".$limit_max : "");
+
+ return _fetch_multirow($where, $sql);
+};
+
+sub _pgsql_write_message {
+ my $server = shift || 'default';
+ my $h = shift;
+ my $rc = 0;
+
+ # PostgreSQL is case sensitive by default. Nice feature,
+ # but it complicates our life tremendously.
+ # Since we want to keep indexes working, the columns in
+ # this list are lowercased before they are inserted. Sigh.
+ my @lowercase = ( 'mailfrom', 'rcpt', 'rcpt_final', 'host_dns', 'host_helo', 'host_rdns' );
+ foreach my $col (@lowercase) {
+ $h->{data}->{$col} = lc($h->{data}->{$col}) if (edt($h->{data},$col));
+ };
+
+ # Special case: we only need to UPDATE the 'completed' field
+ # in the messages table.
+ if ( ($h->{table} eq 'messages') && (exists($h->{data}->{completed})) ) {
+ my $rc = $dbh->do("UPDATE messages SET completed='".$h->{data}->{completed}."' WHERE message_id='".$h->{data}->{message_id}."' AND server='".$server."'");
+ if (defined($rc)) {
+ return 1;
+ }
+ else {
+ # error
+ return 0;
+ };
+ }
+ else {
+ my @fields = sort {$a cmp $b} keys(%{$h->{data}});
+ my @vals = ( "'".$server."'" );
+ foreach (@fields) {
+ my $val = $h->{data}->{$_};
+ $val =~ s/\'/\'\'/g;
+ # shorten $val to limit and remove eventual
+ # trailing quote and backslash characters.
+ $val = substr($val,0,255);
+ $val =~ s/[\\']+$//;
+ push @vals, "'".$val."'";
+ };
+ unshift @fields, 'server';
+
+ my $sql = "INSERT INTO ".$h->{table}.' ("'.join('","',@fields).'") VALUES('.join(',',@vals).")";
+ my $rc = $dbh->do($sql);
+
+ if (defined($rc)) {
+ return 1;
+ }
+ else {
+ return 2 if ($dbh->errstr =~ /duplicate/i);
+ print STDERR "SQL Error (code ".$dbh->err.") on '$h->{table}' with query: $sql\n";
+ return 0;
+ };
+ };
+};
+
+
+# --------------------------------------------------------------------------
+# MySQL functions
+sub _mysql_sql_count {
+ my $where = shift;
+ my $criteria = shift || {};
+
+ my $sql = "SELECT ".
+ "COUNT(*) ".
+ "FROM ".$where.
+ ((scalar keys %{ $criteria } ) ? " "._build_WHERE($criteria) : "" );
+
+ my $sh = $dbh->prepare($sql);
+ $sh->execute;
+ my $tmp = $sh->fetchrow_arrayref();
+ return @{$tmp}[0];
+};
+
+sub _mysql_sql_queue_delete {
+ my $spool_path = shift;
+
+ $dbh->do("DELETE FROM queue WHERE spool_path='$spool_path'");
+};
+
+sub _mysql_sql_queue_update {
+ my $hdr = shift;
+
+ return unless (ref($hdr) eq 'HASH');
+
+ my $server = $hdr->{server};
+ my $message_id = $hdr->{message_id};
+ delete $hdr->{server};
+ delete $hdr->{message_id};
+
+ my @tmp;
+ foreach my $item (keys %{ $hdr }) {
+ my $value = $hdr->{$item};
+ $value =~ s/\'/\'\'/g;
+ $value =~ s/\n/\\n/g;
+ push @tmp, $item.'='."'".$value."'";
+ };
+
+ $dbh->do("UPDATE queue SET ".join(",",@tmp)." WHERE message_id='".$message_id."' AND server='".$server."'");
+};
+
+sub _mysql_sql_queue_add {
+ my $hdr = shift;
+
+ return unless (ref($hdr) eq 'HASH');
+
+ my @fields = sort {$a cmp $b} keys(%{$hdr});
+ my @vals = ();
+ foreach (@fields) {
+ my $val = $hdr->{$_};
+ $val =~ s/\'/\'\'/g;
+ $val =~ s/\n/\\n/g;
+ push @vals, "'".$val."'";
+ };
+
+ $dbh->do("INSERT INTO queue (".join(',',@fields).") VALUES(".join(',',@vals).")");
+};
+
+sub _mysql_sql_queue_set_action {
+ my $server = shift;
+ my $message_id = shift;
+ my $action = shift;
+
+ $dbh->do("UPDATE queue SET action='$action' WHERE server='$server' AND message_id='$message_id'");
+};
+
+sub _mysql_sql_queue_clear_action {
+ my $server = shift;
+ my $message_id = shift;
+
+ $dbh->do("UPDATE queue SET action=NULL WHERE server='$server' AND message_id='$message_id'");
+};
+
+
+sub _mysql_sql_optimize {
+ my $where = shift || "nothing";
+
+ my $sql = "OPTIMIZE TABLE ".$where;
+ my $sh = $dbh->prepare($sql);
+ $sh->execute;
+ $sh->finish;
+
+ return 1;
+};
+
+sub _mysql_sql_delete {
+ my $where = shift || "nothing";
+ my $criteria = shift || {};
+
+ my $sql = "DELETE FROM ".$where.
+ ((scalar keys %{ $criteria } ) ? " "._build_WHERE($criteria) : "" );
+
+ my $sh = $dbh->prepare($sql);
+ my $num = $sh->execute;
+ $sh->finish;
+
+ return (($num eq '0E0') ? 0 : $num);
+};
+
+sub _mysql_sql_select {
+ my $where = shift;
+ my @what = @{ (shift || [ "*" ]) };
+ my $criteria = shift || {};
+ my $order_by = shift || "";
+ my $order_direction = shift || "DESC";
+ my $limit_min = shift;
+ my $limit_max = shift;
+ my $distinct = shift;
+
+ my $sql = "SELECT ".
+ (defined($distinct) ? "DISTINCT " : "").
+ join(", ", @what).
+ " FROM ".$where.
+ ((scalar keys %{ $criteria } ) ? " "._build_WHERE($criteria) : "" ).
+ ($order_by ? " ORDER BY ".$order_by." ".$order_direction : "").
+ (defined($limit_min) ? " LIMIT ".$limit_min : "").
+ (defined($limit_max) ? ",".$limit_max : "");
+
+ return _fetch_multirow($where, $sql);
+};
+
+sub _mysql_write_message {
+ my $server = shift || 'default';
+ my $h = shift;
+ my $rc = 0;
+
+ # Special case: we only need to UPDATE the 'completed' field
+ # in the messages table.
+ if ( ($h->{table} eq 'messages') && (exists($h->{data}->{completed})) ) {
+ my $rc = $dbh->do("UPDATE messages SET completed='".$h->{data}->{completed}."' WHERE message_id='".$h->{data}->{message_id}."' AND server='".$server."'");
+ if (defined($rc)) {
+ return 1;
+ }
+ else {
+ # error
+ return 0;
+ };
+ }
+ else {
+ my @fields = sort {$a cmp $b} keys(%{$h->{data}});
+ my @vals = ( "'".$server."'" );
+ foreach (@fields) {
+ my $val = $h->{data}->{$_};
+ $val =~ s/\'/\'\'/g;
+ # shorten $val to limit and remove eventual
+ # trailing quote and backslash characters.
+ $val = substr($val,0,255);
+ $val =~ s/[\\']+$//;
+ push @vals, "'".$val."'";
+ };
+ unshift @fields, 'server';
+
+ my $sql = "INSERT INTO ".$h->{table}." (".join(',',@fields).") VALUES(".join(',',@vals).")";
+ my $rc = $dbh->do($sql);
+
+ if (defined($rc)) {
+ return 1;
+ }
+ else {
+ # error 1062 means "Duplicate key".
+ return 2 if ($dbh->err == 1062);
+ print STDERR "SQL Error (code ".$dbh->err.") on '$h->{table}' with query: $sql\n";
+ return 0;
+ };
+ };
+};
+
+
+# --------------------------------------------------------------------------
+# misc subroutines used across several DB types
+sub _fetch_multirow {
+ my $table = shift;
+ my $sql = shift;
+ my $limit = shift || 0;
+
+ my $a = [];
+ my $sh = $dbh->prepare($sql);
+ $sh->execute;
+ while (my $tmp = $sh->fetchrow_hashref) {
+ push @{ $a }, $tmp;
+ $limit--;
+ last if ($limit == 0);
+ };
+ $sh->finish;
+
+ return $a;
+};
+
+sub _build_WHERE {
+ my $criteria = shift || {};
+
+ my @set = ();
+ foreach my $col (keys %{ $criteria }) {
+ next unless(defined($criteria->{$col}));
+
+ if ( ($col eq "timestamp") ||
+ ($col eq "completed") ||
+ ($col eq "frozen") ||
+ ($col eq "size") ) {
+ # integer column
+ my ($min,$max) = split / /,$criteria->{$col};
+
+ if (defined($min)) {
+ # greater than X
+ push @set, $col." > ".$min;
+ }
+ if (defined($max)) {
+ # smaller than X
+ push @set, $col." < ".$max;
+ }
+ }
+ elsif (ref($criteria->{$col}) eq 'ARRAY') {
+ # array ref, use exact string match with OR
+ my $str = "( ";
+ foreach my $entry (@{ $criteria->{$col} }) {
+ $str .= " ".$col." = '".$entry."' OR";
+ };
+ chop($str);chop($str);
+ $str .= " )";
+
+ push @set, $str;
+ }
+ else {
+ # string column
+ if (($criteria->{$col} =~ /\%/) || ($criteria->{$col} =~ /\_/)) {
+ # use ILIKE for PGSQL
+ if ($config->{sql}->{type} eq 'pgsql') {
+ push @set, $col." ILIKE '".$criteria->{$col}."'";
+ }
+ else {
+ push @set, $col." LIKE '".$criteria->{$col}."'";
+ };
+ }
+ else {
+ push @set, $col." = '".$criteria->{$col}."'";
+ };
+ };
+ };
+
+ return " WHERE ".join(" AND ", @set);
+};
+
+
+1;
diff --git a/exilog_stylesheet.css b/exilog_stylesheet.css
new file mode 100644
index 0000000..f50ec95
--- /dev/null
+++ b/exilog_stylesheet.css
@@ -0,0 +1,233 @@
+body {
+ margin: 0;
+ padding: 16px 0px 16px 0px;
+ background: #333333;
+}
+div.body {
+ margin: 0;
+ padding: 0;
+ width: 960px;
+ background: #bbeebb;
+ font-family: Arial, Helvetica, Sans-Serif;
+ font-size: 12px;
+}
+
+table {
+ font-family: Arial, Helvetica, Sans-Serif;
+ font-size: 12px;
+}
+img {
+ padding: 0;
+ margin: 0;
+ border: 0;
+ display: block;
+}
+div.top_spacer {
+ padding: 16px 0px 0px 0px;
+ margin: 0;
+ border: 0;
+}
+
+div.display {
+ padding: 0px 16px 16px 16px;
+ /* border-left: 1px solid #ccffcc; */
+ /* border-right: 1px solid #449922; */
+ /* border-bottom: 1px solid #449922; */
+}
+table.logo {
+ width: 100%;
+ background: #333333;
+ color: white;
+}
+td.logo_title {
+ font-size: 16px;
+ font-weight: bold;
+}
+td.logo_text {
+ font-size: 12px;
+ vertical-align: top;
+ text-align: right;
+}
+
+table.tabs {
+ width: 100%;
+}
+td.tabs_static {
+ background: #ff5500;
+ color: white;
+ border-top: 1px solid #ee6600;
+ border-left: 1px solid #ee6600;
+ /* border-bottom: 1px solid #ccffcc; */
+}
+td.tabs_click {
+ font-size: 16px;
+ text-align: center;
+ border-top: 1px solid #339944;
+ border-left: 1px solid #339944;
+ /* border-bottom: 1px solid #ccffcc; */
+ background: #227733;
+ color: #dddddd;
+ cursor: pointer;
+ cursor: hand;
+}
+td.tabs_active {
+ font-size: 16px;
+ font-weight: bold;
+ text-align: center;
+ border-top: 1px solid #ccffcc;
+ border-left: 1px solid #ccffcc;
+ background: #bbeebb;
+ color: black;
+}
+td.tabs_spacer {
+ /* border-bottom: 1px solid #ccffcc; */
+ background: #333333;
+}
+
+input {
+ font-size: 10px;
+}
+select {
+ font-size: 10px;
+}
+
+table.queue_table {
+ background: black;
+ font-size: 12px;
+}
+td.queue_header {
+ background: #cccccc;
+ font-weight: bold;
+ padding: 2px 4px 2px 4px;
+}
+td.queue {
+ background: #eeeeee;
+ white-space: nowrap;
+ padding: 2px 4px 2px 4px;
+}
+div.rcpts_pending_popup {
+ position: absolute;
+ visibility: hidden;
+ background: #dddddd;
+ color: black;
+ padding: 4px;
+ border: 1px solid black;
+ width: 100px;
+}
+
+td.table_titlebar {
+ padding: 2;
+ background: #ffffff;
+}
+
+td.table_arrival {
+ padding: 2;
+ background: #eeeeee;
+}
+
+td.table_queue {
+ padding: 2;
+ background: #eeeeee;
+}
+
+td.table_delivery {
+ padding: 2;
+ background: #eeeeee;
+}
+
+td.table_error {
+ padding: 2;
+ background: #eeeeee;
+}
+
+td.table_deferral {
+ padding: 2;
+ background: #eeeeee;
+}
+
+td.table_unknown {
+ padding: 2;
+ background: #eeeeee;
+}
+
+td.table_reject {
+ padding: 2;
+ background: #eeeeee;
+}
+
+table.stats {
+ width: 926px;
+ background: #999999;
+ margin: 0;
+}
+td.table_stats {
+ padding: 8px 4px 8px 4px;
+ background: #eeeeee;
+}
+td.stats {
+ padding-left: 6px;
+ padding-right: 6px;
+ text-align: left;
+ width: 1%;
+}
+
+table.message {
+ width: 926px;
+ background: #999999;
+ margin: 0;
+}
+td.message {
+ padding-left: 6px;
+ padding-right: 6px;
+ text-align: center;
+ width: 1%;
+}
+td.message_wide {
+ padding-left: 6px;
+ padding-right: 6px;
+ text-align: left;
+}
+
+table.header {
+ width: 926px;
+ background: #999999;
+ margin: 0;
+}
+td.header {
+ color: white;
+ background: black;
+ padding: 4px 4px 4px 4px;
+}
+
+
+td.large_icon {
+ padding: 0px 4px 0px 4px;
+ white-space: nowrap;
+}
+td.large_text {
+ padding: 0px 8px 0px 0px;
+ font-size: 24px;
+ font-weight: bold;
+}
+
+
+table.item {
+ font-family: monospace;
+ border: 0;
+ padding: 0;
+ margin: 0;
+}
+tr.item_link {
+ cursor: hand;
+ cursor: pointer;
+}
+td.item_icon {
+ padding: 0px 0px 0px 4px;
+ vertical-align: top;
+}
+td.item_text {
+ padding: 0px 4px 0px 4px;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
diff --git a/exilog_util.pm b/exilog_util.pm
new file mode 100644
index 0000000..c50e679
--- /dev/null
+++ b/exilog_util.pm
@@ -0,0 +1,136 @@
+#!/usr/bin/perl -w
+#
+# This file is part of the exilog suite.
+#
+# http://duncanthrax.net/exilog/
+#
+# (c) Tom Kistner 2004
+#
+# See LICENSE for licensing information.
+#
+
+package exilog_util;
+use Time::Local;
+use POSIX qw( strftime );
+use strict;
+use exilog_config;
+
+BEGIN {
+ use Exporter;
+ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+ # set the version for version checking
+ $VERSION = 0.1;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(
+ &edt
+ &edv
+ &ina
+ &date_to_stamp
+ &stamp_to_date
+ &human_size
+ );
+
+ %EXPORT_TAGS = ();
+
+ # your exported package globals go here,
+ # as well as any optionally exported functions
+ @EXPORT_OK = qw();
+}
+
+
+# checks if scalar is in array
+sub ina {
+ my $aref = shift || [];
+ my $str = shift || "";
+
+ unless (ref($aref) eq 'ARRAY') {
+ $aref = [ $aref ];
+ };
+
+ foreach (@{ $aref }) {
+ return 1 if ($_ eq $str);
+ };
+ return 0;
+};
+
+
+# exists, defined and true (in perl sense)
+sub edt {
+ my $h = shift;
+ my $hkey = shift;
+ return 0 unless (ref($h) eq 'HASH');
+ return 1 if ( exists($h->{$hkey}) &&
+ defined($h->{$hkey}) &&
+ $h->{$hkey} );
+ return 0;
+};
+
+
+# exists, defined and valid (that is, not empty)
+sub edv {
+ my $h = shift;
+ my $hkey = shift;
+ return 0 unless (ref($h) eq 'HASH');
+ return 1 if ( exists($h->{$hkey}) &&
+ defined($h->{$hkey}) &&
+ $h->{$hkey} ne '' );
+ return 0;
+};
+
+
+sub date_to_stamp {
+ my $date = shift || "";
+ my $tod = shift || "00:00:00";
+ my ($year,$month,$mday) = split /\-/, $date;
+ my ($hour,$minute,$second,$junk) = split /[: ]/, $tod;
+ $year-=1900;
+ $month--;
+
+ # This is for parsing timestamps that include GMT offsets
+ if (edv($junk)) {
+ my $hoff = ($junk =~ /[-+](\d\d)\d\d/);
+ my $moff = ($junk =~ /[-+]\d\d(\d\d)/);
+ if ($junk =~ /\+/) {
+ $hour = $hour - $hoff;
+ $minute = $minute - $moff;
+ }
+ else {
+ $hour = $hour + $hoff;
+ $minute = $minute + $moff;
+ }
+ };
+
+ if ($config->{web}->{timestamps} eq 'local') {
+ return timelocal($second,$minute,$hour,$mday,$month,$year);
+ }
+ else {
+ return timegm($second,$minute,$hour,$mday,$month,$year);
+ };
+};
+
+
+sub stamp_to_date {
+ my $stamp = shift;
+ my $no_seconds = shift || 0;
+ # convert to date/time string
+ if ($config->{web}->{timestamps} eq 'local') {
+ return ($no_seconds ? strftime("%Y-%m-%d %H:%M",localtime($stamp)) : strftime("%Y-%m-%d %H:%M:%S",localtime($stamp)));
+ }
+ else {
+ return ($no_seconds ? strftime("%Y-%m-%d %H:%M",gmtime($stamp)) : strftime("%Y-%m-%d %H:%M:%S",gmtime($stamp)));
+ };
+};
+
+sub human_size {
+ my $size = shift;
+ my @units = ( '', 'k', 'M', 'G' );
+ while ( ($size > 9999) && ((scalar @units) > 1) ) {
+ shift @units;
+ $size = int($size/1024);
+ };
+ return $size.$units[0];
+};
+
+
+1;
diff --git a/icons/address.png b/icons/address.png
new file mode 100644
index 0000000..e572432
--- /dev/null
+++ b/icons/address.png
Binary files differ
diff --git a/icons/arrival.png b/icons/arrival.png
new file mode 100644
index 0000000..fb2b15d
--- /dev/null
+++ b/icons/arrival.png
Binary files differ
diff --git a/icons/arrival_auth.png b/icons/arrival_auth.png
new file mode 100644
index 0000000..ca0fdab
--- /dev/null
+++ b/icons/arrival_auth.png
Binary files differ
diff --git a/icons/arrival_local.png b/icons/arrival_local.png
new file mode 100644
index 0000000..28c0692
--- /dev/null
+++ b/icons/arrival_local.png
Binary files differ
diff --git a/icons/arrival_normal.png b/icons/arrival_normal.png
new file mode 100644
index 0000000..2d9dcdb
--- /dev/null
+++ b/icons/arrival_normal.png
Binary files differ
diff --git a/icons/arrival_tls.png b/icons/arrival_tls.png
new file mode 100644
index 0000000..508d994
--- /dev/null
+++ b/icons/arrival_tls.png
Binary files differ
diff --git a/icons/arrival_tls_auth.png b/icons/arrival_tls_auth.png
new file mode 100644
index 0000000..46bda32
--- /dev/null
+++ b/icons/arrival_tls_auth.png
Binary files differ
diff --git a/icons/deferral_normal.png b/icons/deferral_normal.png
new file mode 100644
index 0000000..d486b54
--- /dev/null
+++ b/icons/deferral_normal.png
Binary files differ
diff --git a/icons/deferral_tls.png b/icons/deferral_tls.png
new file mode 100644
index 0000000..6ac4421
--- /dev/null
+++ b/icons/deferral_tls.png
Binary files differ
diff --git a/icons/deferred.png b/icons/deferred.png
new file mode 100644
index 0000000..648da6b
--- /dev/null
+++ b/icons/deferred.png
Binary files differ
diff --git a/icons/delivered.png b/icons/delivered.png
new file mode 100644
index 0000000..6edfd8e
--- /dev/null
+++ b/icons/delivered.png
Binary files differ
diff --git a/icons/delivery.png b/icons/delivery.png
new file mode 100644
index 0000000..a53017c
--- /dev/null
+++ b/icons/delivery.png
Binary files differ
diff --git a/icons/delivery_normal.png b/icons/delivery_normal.png
new file mode 100644
index 0000000..beefb44
--- /dev/null
+++ b/icons/delivery_normal.png
Binary files differ
diff --git a/icons/delivery_tls.png b/icons/delivery_tls.png
new file mode 100644
index 0000000..eb257cd
--- /dev/null
+++ b/icons/delivery_tls.png
Binary files differ
diff --git a/icons/dns.png b/icons/dns.png
new file mode 100644
index 0000000..d0acd1f
--- /dev/null
+++ b/icons/dns.png
Binary files differ
diff --git a/icons/dsn_warning.png b/icons/dsn_warning.png
new file mode 100644
index 0000000..33649fb
--- /dev/null
+++ b/icons/dsn_warning.png
Binary files differ
diff --git a/icons/errmsg.png b/icons/errmsg.png
new file mode 100644
index 0000000..b4ed0dc
--- /dev/null
+++ b/icons/errmsg.png
Binary files differ
diff --git a/icons/error.png b/icons/error.png
new file mode 100644
index 0000000..d5a85c5
--- /dev/null
+++ b/icons/error.png
Binary files differ
diff --git a/icons/error_normal.png b/icons/error_normal.png
new file mode 100644
index 0000000..2f96b0c
--- /dev/null
+++ b/icons/error_normal.png
Binary files differ
diff --git a/icons/error_tls.png b/icons/error_tls.png
new file mode 100644
index 0000000..25ac787
--- /dev/null
+++ b/icons/error_tls.png
Binary files differ
diff --git a/icons/event_type.png b/icons/event_type.png
new file mode 100644
index 0000000..2c8600d
--- /dev/null
+++ b/icons/event_type.png
Binary files differ
diff --git a/icons/find.png b/icons/find.png
new file mode 100644
index 0000000..e912de2
--- /dev/null
+++ b/icons/find.png
Binary files differ
diff --git a/icons/frozen.png b/icons/frozen.png
new file mode 100644
index 0000000..b4ed0dc
--- /dev/null
+++ b/icons/frozen.png
Binary files differ
diff --git a/icons/helo.png b/icons/helo.png
new file mode 100644
index 0000000..075d794
--- /dev/null
+++ b/icons/helo.png
Binary files differ
diff --git a/icons/ident.png b/icons/ident.png
new file mode 100644
index 0000000..6d6ac94
--- /dev/null
+++ b/icons/ident.png
Binary files differ
diff --git a/icons/queue_deferred.png b/icons/queue_deferred.png
new file mode 100644
index 0000000..030f2c0
--- /dev/null
+++ b/icons/queue_deferred.png
Binary files differ
diff --git a/icons/queue_frozen.png b/icons/queue_frozen.png
new file mode 100644
index 0000000..4822aad
--- /dev/null
+++ b/icons/queue_frozen.png
Binary files differ
diff --git a/icons/queue_normal.png b/icons/queue_normal.png
new file mode 100644
index 0000000..47e59f2
--- /dev/null
+++ b/icons/queue_normal.png
Binary files differ
diff --git a/icons/queued.png b/icons/queued.png
new file mode 100644
index 0000000..fc3d105
--- /dev/null
+++ b/icons/queued.png
Binary files differ
diff --git a/icons/reject_postdata.png b/icons/reject_postdata.png
new file mode 100644
index 0000000..ae84faa
--- /dev/null
+++ b/icons/reject_postdata.png
Binary files differ
diff --git a/icons/reject_predata.png b/icons/reject_predata.png
new file mode 100644
index 0000000..51a7d70
--- /dev/null
+++ b/icons/reject_predata.png
Binary files differ
diff --git a/icons/router_transport.png b/icons/router_transport.png
new file mode 100644
index 0000000..98d4b99
--- /dev/null
+++ b/icons/router_transport.png
Binary files differ
diff --git a/icons/server.png b/icons/server.png
new file mode 100644
index 0000000..5379dfb
--- /dev/null
+++ b/icons/server.png
Binary files differ
diff --git a/icons/server_normal.png b/icons/server_normal.png
new file mode 100644
index 0000000..ccf34fc
--- /dev/null
+++ b/icons/server_normal.png
Binary files differ
diff --git a/icons/size.png b/icons/size.png
new file mode 100644
index 0000000..c43d690
--- /dev/null
+++ b/icons/size.png
Binary files differ
diff --git a/icons/stats_h24.png b/icons/stats_h24.png
new file mode 100644
index 0000000..e71e752
--- /dev/null
+++ b/icons/stats_h24.png
Binary files differ
diff --git a/icons/stopwatch.png b/icons/stopwatch.png
new file mode 100644
index 0000000..786bf44
--- /dev/null
+++ b/icons/stopwatch.png
Binary files differ
diff --git a/icons/timerange.png b/icons/timerange.png
new file mode 100644
index 0000000..1094f75
--- /dev/null
+++ b/icons/timerange.png
Binary files differ
diff --git a/icons/unknown.png b/icons/unknown.png
new file mode 100644
index 0000000..27fc48e
--- /dev/null
+++ b/icons/unknown.png
Binary files differ