diff options
-rw-r--r-- | LICENSE | 340 | ||||
-rw-r--r-- | VERSION | 1 | ||||
-rw-r--r-- | doc/Changelog | 31 | ||||
-rw-r--r-- | doc/exilog.txt | 171 | ||||
-rw-r--r-- | doc/mysql-db-script.sql | 199 | ||||
-rw-r--r-- | doc/pgsql-db-script.sql | 624 | ||||
-rw-r--r-- | exilog.conf-example | 143 | ||||
-rwxr-xr-x | exilog_agent.pl | 471 | ||||
-rwxr-xr-x | exilog_cgi.pl | 124 | ||||
-rw-r--r-- | exilog_cgi_html.pm | 984 | ||||
-rw-r--r-- | exilog_cgi_messages.pm | 816 | ||||
-rw-r--r-- | exilog_cgi_param.pm | 74 | ||||
-rw-r--r-- | exilog_cgi_queues.pm | 131 | ||||
-rw-r--r-- | exilog_cgi_servers.pm | 102 | ||||
-rwxr-xr-x | exilog_cleanup.pl | 65 | ||||
-rw-r--r-- | exilog_config.pm | 63 | ||||
-rw-r--r-- | exilog_jscript.js | 1232 | ||||
-rw-r--r-- | exilog_parse.pm | 351 | ||||
-rw-r--r-- | exilog_sql.pm | 556 | ||||
-rw-r--r-- | exilog_stylesheet.css | 233 | ||||
-rw-r--r-- | exilog_util.pm | 136 | ||||
-rw-r--r-- | icons/address.png | bin | 0 -> 630 bytes | |||
-rw-r--r-- | icons/arrival.png | bin | 0 -> 935 bytes | |||
-rw-r--r-- | icons/arrival_auth.png | bin | 0 -> 2246 bytes | |||
-rw-r--r-- | icons/arrival_local.png | bin | 0 -> 2240 bytes | |||
-rw-r--r-- | icons/arrival_normal.png | bin | 0 -> 1801 bytes | |||
-rw-r--r-- | icons/arrival_tls.png | bin | 0 -> 2172 bytes | |||
-rw-r--r-- | icons/arrival_tls_auth.png | bin | 0 -> 2600 bytes | |||
-rw-r--r-- | icons/deferral_normal.png | bin | 0 -> 1977 bytes | |||
-rw-r--r-- | icons/deferral_tls.png | bin | 0 -> 2354 bytes | |||
-rw-r--r-- | icons/deferred.png | bin | 0 -> 924 bytes | |||
-rw-r--r-- | icons/delivered.png | bin | 0 -> 481 bytes | |||
-rw-r--r-- | icons/delivery.png | bin | 0 -> 938 bytes | |||
-rw-r--r-- | icons/delivery_normal.png | bin | 0 -> 1842 bytes | |||
-rw-r--r-- | icons/delivery_tls.png | bin | 0 -> 2235 bytes | |||
-rw-r--r-- | icons/dns.png | bin | 0 -> 991 bytes | |||
-rw-r--r-- | icons/dsn_warning.png | bin | 0 -> 760 bytes | |||
-rw-r--r-- | icons/errmsg.png | bin | 0 -> 775 bytes | |||
-rw-r--r-- | icons/error.png | bin | 0 -> 957 bytes | |||
-rw-r--r-- | icons/error_normal.png | bin | 0 -> 1772 bytes | |||
-rw-r--r-- | icons/error_tls.png | bin | 0 -> 2176 bytes | |||
-rw-r--r-- | icons/event_type.png | bin | 0 -> 816 bytes | |||
-rw-r--r-- | icons/find.png | bin | 0 -> 688 bytes | |||
-rw-r--r-- | icons/frozen.png | bin | 0 -> 775 bytes | |||
-rw-r--r-- | icons/helo.png | bin | 0 -> 883 bytes | |||
-rw-r--r-- | icons/ident.png | bin | 0 -> 712 bytes | |||
-rw-r--r-- | icons/queue_deferred.png | bin | 0 -> 2093 bytes | |||
-rw-r--r-- | icons/queue_frozen.png | bin | 0 -> 2031 bytes | |||
-rw-r--r-- | icons/queue_normal.png | bin | 0 -> 1562 bytes | |||
-rw-r--r-- | icons/queued.png | bin | 0 -> 710 bytes | |||
-rw-r--r-- | icons/reject_postdata.png | bin | 0 -> 1996 bytes | |||
-rw-r--r-- | icons/reject_predata.png | bin | 0 -> 2086 bytes | |||
-rw-r--r-- | icons/router_transport.png | bin | 0 -> 1041 bytes | |||
-rw-r--r-- | icons/server.png | bin | 0 -> 790 bytes | |||
-rw-r--r-- | icons/server_normal.png | bin | 0 -> 1791 bytes | |||
-rw-r--r-- | icons/size.png | bin | 0 -> 765 bytes | |||
-rw-r--r-- | icons/stats_h24.png | bin | 0 -> 2706 bytes | |||
-rw-r--r-- | icons/stopwatch.png | bin | 0 -> 1004 bytes | |||
-rw-r--r-- | icons/timerange.png | bin | 0 -> 1007 bytes | |||
-rw-r--r-- | icons/unknown.png | bin | 0 -> 717 bytes |
60 files changed, 6847 insertions, 0 deletions
@@ -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. @@ -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"}," "). + (($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"}," "). + $q->td({-align=>"center",-class=>"tabs_static",-style=>"font-size: 12px; width: 240px; white-space: nowrap;" }, " Server time".(($config->{web}->{timestamps} eq 'gmt') ? " (GMT)":"").": ".stamp_to_date(time())." ") + ) + ); + + 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)" } ) + : + " " + ) + ) + ), + $q->Tr( + $q->td({-class=>"stats"}, + ( $num_queued->{frozen} ? + _item( { 'icon' => "icons/frozen.png" }, + { 'text' => $num_queued->{frozen}." frozen (".$num_queued->{frozen_bounce}." bounces)" } ) + : + " " + ) + ) + ) + ) + ) + ), + $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' => '·' } : 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' => '·' } : 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' => '·' } : 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' => ' ' }, + { 'icon' => "icons/deferred.png", + 'title' => join("\n",@recipients_pending) }, + { 'text' => scalar @recipients_pending }, + { 'text' => ' ' }, + { '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%"}, + " " + ), + $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/\>/\>\;/g; + $part->{text} =~ s/\</\<\;/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(" <b>|</b> ",@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> </td></tr>'; + }; + $num++; + if (edt($config->{servers}->{$server},'group')) { + $groups->{$config->{servers}->{$server}->{group}} = '{'.$config->{servers}->{$server}->{group}.'}'; + }; + }; + if (($num % 4) != 0) { + $html .= '<td> </td>' x ((4-($num % 4))*2); + $html .= '<td> </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" })." - ". + $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> </td></tr>'; + }; + $num++; + if (edt($config->{servers}->{$server},'group')) { + $groups->{$config->{servers}->{$server}->{group}} = '{'.$config->{servers}->{$server}->{group}.'}'; + }; + }; + if (($num % 4) != 0) { + $html .= '<td> </td>' x ((4-($num % 4))*2); + $html .= '<td> </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 = ' '; + }; +}; + +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"> </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+');"><</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+');">></A></TD>'; + result += '<TD CLASS="'+this.cssPrefix+'cpMonthNavigation" WIDTH="10"> </TD>'; + + result += '<TD CLASS="'+this.cssPrefix+'cpYearNavigation" WIDTH="10"><A CLASS="'+this.cssPrefix+'cpYearNavigation" HREF="'+refreshLink+'('+this.index+','+month+','+(year-1)+');"><</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)+');">></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+');"><<</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+');">>></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)+');"><<</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)+');">>></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))+');"><<</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))+');">>></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 Binary files differnew file mode 100644 index 0000000..e572432 --- /dev/null +++ b/icons/address.png diff --git a/icons/arrival.png b/icons/arrival.png Binary files differnew file mode 100644 index 0000000..fb2b15d --- /dev/null +++ b/icons/arrival.png diff --git a/icons/arrival_auth.png b/icons/arrival_auth.png Binary files differnew file mode 100644 index 0000000..ca0fdab --- /dev/null +++ b/icons/arrival_auth.png diff --git a/icons/arrival_local.png b/icons/arrival_local.png Binary files differnew file mode 100644 index 0000000..28c0692 --- /dev/null +++ b/icons/arrival_local.png diff --git a/icons/arrival_normal.png b/icons/arrival_normal.png Binary files differnew file mode 100644 index 0000000..2d9dcdb --- /dev/null +++ b/icons/arrival_normal.png diff --git a/icons/arrival_tls.png b/icons/arrival_tls.png Binary files differnew file mode 100644 index 0000000..508d994 --- /dev/null +++ b/icons/arrival_tls.png diff --git a/icons/arrival_tls_auth.png b/icons/arrival_tls_auth.png Binary files differnew file mode 100644 index 0000000..46bda32 --- /dev/null +++ b/icons/arrival_tls_auth.png diff --git a/icons/deferral_normal.png b/icons/deferral_normal.png Binary files differnew file mode 100644 index 0000000..d486b54 --- /dev/null +++ b/icons/deferral_normal.png diff --git a/icons/deferral_tls.png b/icons/deferral_tls.png Binary files differnew file mode 100644 index 0000000..6ac4421 --- /dev/null +++ b/icons/deferral_tls.png diff --git a/icons/deferred.png b/icons/deferred.png Binary files differnew file mode 100644 index 0000000..648da6b --- /dev/null +++ b/icons/deferred.png diff --git a/icons/delivered.png b/icons/delivered.png Binary files differnew file mode 100644 index 0000000..6edfd8e --- /dev/null +++ b/icons/delivered.png diff --git a/icons/delivery.png b/icons/delivery.png Binary files differnew file mode 100644 index 0000000..a53017c --- /dev/null +++ b/icons/delivery.png diff --git a/icons/delivery_normal.png b/icons/delivery_normal.png Binary files differnew file mode 100644 index 0000000..beefb44 --- /dev/null +++ b/icons/delivery_normal.png diff --git a/icons/delivery_tls.png b/icons/delivery_tls.png Binary files differnew file mode 100644 index 0000000..eb257cd --- /dev/null +++ b/icons/delivery_tls.png diff --git a/icons/dns.png b/icons/dns.png Binary files differnew file mode 100644 index 0000000..d0acd1f --- /dev/null +++ b/icons/dns.png diff --git a/icons/dsn_warning.png b/icons/dsn_warning.png Binary files differnew file mode 100644 index 0000000..33649fb --- /dev/null +++ b/icons/dsn_warning.png diff --git a/icons/errmsg.png b/icons/errmsg.png Binary files differnew file mode 100644 index 0000000..b4ed0dc --- /dev/null +++ b/icons/errmsg.png diff --git a/icons/error.png b/icons/error.png Binary files differnew file mode 100644 index 0000000..d5a85c5 --- /dev/null +++ b/icons/error.png diff --git a/icons/error_normal.png b/icons/error_normal.png Binary files differnew file mode 100644 index 0000000..2f96b0c --- /dev/null +++ b/icons/error_normal.png diff --git a/icons/error_tls.png b/icons/error_tls.png Binary files differnew file mode 100644 index 0000000..25ac787 --- /dev/null +++ b/icons/error_tls.png diff --git a/icons/event_type.png b/icons/event_type.png Binary files differnew file mode 100644 index 0000000..2c8600d --- /dev/null +++ b/icons/event_type.png diff --git a/icons/find.png b/icons/find.png Binary files differnew file mode 100644 index 0000000..e912de2 --- /dev/null +++ b/icons/find.png diff --git a/icons/frozen.png b/icons/frozen.png Binary files differnew file mode 100644 index 0000000..b4ed0dc --- /dev/null +++ b/icons/frozen.png diff --git a/icons/helo.png b/icons/helo.png Binary files differnew file mode 100644 index 0000000..075d794 --- /dev/null +++ b/icons/helo.png diff --git a/icons/ident.png b/icons/ident.png Binary files differnew file mode 100644 index 0000000..6d6ac94 --- /dev/null +++ b/icons/ident.png diff --git a/icons/queue_deferred.png b/icons/queue_deferred.png Binary files differnew file mode 100644 index 0000000..030f2c0 --- /dev/null +++ b/icons/queue_deferred.png diff --git a/icons/queue_frozen.png b/icons/queue_frozen.png Binary files differnew file mode 100644 index 0000000..4822aad --- /dev/null +++ b/icons/queue_frozen.png diff --git a/icons/queue_normal.png b/icons/queue_normal.png Binary files differnew file mode 100644 index 0000000..47e59f2 --- /dev/null +++ b/icons/queue_normal.png diff --git a/icons/queued.png b/icons/queued.png Binary files differnew file mode 100644 index 0000000..fc3d105 --- /dev/null +++ b/icons/queued.png diff --git a/icons/reject_postdata.png b/icons/reject_postdata.png Binary files differnew file mode 100644 index 0000000..ae84faa --- /dev/null +++ b/icons/reject_postdata.png diff --git a/icons/reject_predata.png b/icons/reject_predata.png Binary files differnew file mode 100644 index 0000000..51a7d70 --- /dev/null +++ b/icons/reject_predata.png diff --git a/icons/router_transport.png b/icons/router_transport.png Binary files differnew file mode 100644 index 0000000..98d4b99 --- /dev/null +++ b/icons/router_transport.png diff --git a/icons/server.png b/icons/server.png Binary files differnew file mode 100644 index 0000000..5379dfb --- /dev/null +++ b/icons/server.png diff --git a/icons/server_normal.png b/icons/server_normal.png Binary files differnew file mode 100644 index 0000000..ccf34fc --- /dev/null +++ b/icons/server_normal.png diff --git a/icons/size.png b/icons/size.png Binary files differnew file mode 100644 index 0000000..c43d690 --- /dev/null +++ b/icons/size.png diff --git a/icons/stats_h24.png b/icons/stats_h24.png Binary files differnew file mode 100644 index 0000000..e71e752 --- /dev/null +++ b/icons/stats_h24.png diff --git a/icons/stopwatch.png b/icons/stopwatch.png Binary files differnew file mode 100644 index 0000000..786bf44 --- /dev/null +++ b/icons/stopwatch.png diff --git a/icons/timerange.png b/icons/timerange.png Binary files differnew file mode 100644 index 0000000..1094f75 --- /dev/null +++ b/icons/timerange.png diff --git a/icons/unknown.png b/icons/unknown.png Binary files differnew file mode 100644 index 0000000..27fc48e --- /dev/null +++ b/icons/unknown.png |