geximon-0.7.7/0000755000175000017500000000000011036202732012127 5ustar daviddavidgeximon-0.7.7/LICENCE0000644000175000017500000004310510470723547013134 0ustar daviddavid 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. Copyright (C) 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. , 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. geximon-0.7.7/README0000644000175000017500000000334610470723547013032 0ustar daviddavidgeximon 0.7 by Gintautas Miliauskas Programmers of Vilnius Website: http://pov.lt/geximon >>> Introduction <<< geximon is a replacement for eximon, the exim server monitor. The original program is functional, but, to cite one of the exim mailing list members, "it could use a visit from one of the Learning Channel redecoration shows :-)". geximon is written in Python and is based on Gtk2. The source code is available under the GNU General Public Licence. This is a development version, so your mileage may vary, however, there should not be any serious breakage. If you happen to come across a bug, please take the time to drop me an e-mail. You will need Python[1] (version >= 2.2) and a recent version of pygtk[2]. Of course, you will need exim[3] (version 3 or 4) installed as well. If exim is installed in a non-standard location, you will need to specify the paths in geximon preferences. [1] http://www.python.org [2] http://www.daa.com.au/~james/software/pygtk/ [3] http://www.exim.org This version has been tested with Debian testing and unstable. >>> Installation <<< You may run the program by running `geximon.py` in the geximon directory. Moreover, as this program is packaged using distutils, you can easily install it by running `./setup.py install` as the superuser. A script named `geximon` will be placed in your path which you can then use to invoke the program. If you are using a Debian-based operating system, you should use the Debian package rather than distutils. The package is available at the website. More documentation is available in HTML format in the `doc/` subdirectory. It should also be accessible from geximon through the Help menu. geximon-0.7.7/doc/0000755000175000017500000000000010507446615012710 5ustar daviddavidgeximon-0.7.7/doc/Makefile0000644000175000017500000000042410470723547014351 0ustar daviddavidall: geximon.html geximon.txt geximon.html: geximon-C.xml db2html --nochunks geximon-C.xml mv geximon-C/geximon-C.html geximon.html rm -rf geximon-C/ geximon-C.junk/ geximon.txt: geximon.html lynx -dump geximon.html > geximon.txt clean: rm -f geximon.txt geximon.html geximon-0.7.7/doc/geximon-C.omf0000644000175000017500000000105410470723547015242 0ustar daviddavid Gintautas Miliauskas David Watson The gEximon Manual 2004-03-22 geximon-0.7.7/doc/geximon-C.xml0000644000175000017500000001454110470723547015266 0ustar daviddavid
gEximon documentation Introduction geximon is a replacement for eximon, the exim server monitor. The original program is functional, but, to cite one of the exim mailing list members, "it could use a visit from one of the Learning Channel redecoration shows :-)". geximon is written in Python and is based on Gtk2. The source code is available under the GNU General Public Licence. This is a development version, so your mileage may vary, however, there should not be any serious breakage. If you happen to come across a bug or if you have any suggestions, please take the time to drop me an e-mail to david@planetwatson.co.uk. You will need Python (version >= 2.2) and a recent version of pygtk. Of course, you will need exim (version 3 or 4) installed as well. If exim is installed in a non-standard location, you will need to specify the paths in geximon preferences. Installation You may run the program by running geximon.py in the geximon directory. Moreover, as this program is packaged using distutils, you can easily install it by running ./setup.py install as the superuser. A script named geximon will be placed in your path which you can then use to invoke the program. Configuration geximon stores its configuration in the file .geximonrc in the user's home directory. Most settings as well as window positions are saved in this file in a human-readable format, however, there should be no need to edit it manually. Log files can also be fetched from a remote server. In order to use this feature you will need to setup non-console authentication. Privileges geximon needs privileges to communicate with exim and to read its logs. The easiest solution is to run the program as the superuser. If you are concerned about security, and prefer to run the program as an ordinary user, there are a few things you can do. Adding yourself to 'mail' and 'adm' groups might give enough priviledges to see the exim queue (try /usr/sbin/exim -bp) and to read the main logfile (/var/log/exim/mainlog). However, the process list will not be available (it needs to run /usr/sbin/exiwhat). You can also use sudo (a program that allows controlled permission elevation) to great effect. You will need to configure sudo not to ask for a password for some commands. As the superuser, execute visudo to edit the sudo configuration file (/etc/sudoers), and add this line: your_username ALL=(root) NOPASSWD: /usr/sbin/exim, /usr/sbin/exiwhat [For more information about the file syntax, see the manual page of sudoers(5).] Now, in geximon preferences, check the 'Use sudo' option. The changes should take effect immediately after applying the preferences window: the queue list and process list should start working. Note that you will still not be able to kill exim processes from geximon, to do that you must run geximon itself as the superuser. Components that lack permissions to function properly should not interfere with others (e.g. if you can read the logfile, the log should be shown even if the queue can not be listed.) Usage The user interface should be fairly intuitive. One thing that might not be apparent at first glance is that messages with ID's highlighted in red are frozen, which means that they could not be delivered, but delivery will be attempted after some time passes. Context menus are available in the queue list and the process list. You may hold down Ctrl to select multiple items and use Shift to select ranges; Ctrl A marks all items. If the confirmation and report popups annoy you, they can be turned off in preferences. eximstats and exigrep are two quite useful utilities that are bundled with exim. They can be invoked through the 'Exim' menu. exigrep can also be invoked on fields of messages by using the context menu of the queue list. You can choose the text wrapping mode in the context menu of textboxes. If you are told a message is locked, it means that it is being processed by exim. To unlock it, you may want to use the process list to seek out the exim instance that is currently dealing with the message and send it a SIGTERM. The exim instance will terminate and the message should now be unlocked. The plots have the same meanings as in the original eximon. The label colors correspond to the line colors. The number next to a label is the last value for that plot, visual scaling (if >1) is shown in brackets. If you see a single line only, it's probably because it simply overdraws the other lines. When the process list is enabled, exiwhat is invoked regularly (the interval can be specified in preferences). Each run may produce a short but noticeable spike in CPU usage, so closing the window when it is not in use is a good idea.
geximon-0.7.7/doc/geximon.80000644000175000017500000000137510470723547014456 0ustar daviddavid.TH GEXIMON 8 "April 7, 2004" .SH NAME geximon \- a Gtk2 exim MTA monitor .SH SYNOPSIS .B geximon .SH DESCRIPTION \fBgeximon\fP is a Gtk2 monitor program for the exim mail transfer agent. In functionality it is similar to \fBeximon\fP. It has all features of the original (tracking the log, managing the mail queue, plotting, etc.) and more. It also integrates with \fBexiwhat\fP (allowing you to manage active exim instances), \fBexigrep\fP, and \fBeximstats\fP. The interface is intuitive, compact, and easy to use. .SH OPTIONS .TP \fBgeximon\fP ignores all command-line arguments. .SH SEE ALSO .BR exim (8), .BR eximon (8), .BR exiwhat (8), .BR exigrep (8), .BR eximstats (8). .br .SH AUTHOR This manual page was written by Gintautas Miliauskas . geximon-0.7.7/doc/geximon.html0000644000175000017500000001614110470723547015250 0ustar daviddavid gEximon documentation

1. Introduction

geximon is a replacement for eximon, the exim server monitor. The original program is functional, but, to cite one of the exim mailing list members, "it could use a visit from one of the Learning Channel redecoration shows :-)".

geximon is written in Python and is based on Gtk2. The source code is available under the GNU General Public Licence. This is a development version, so your mileage may vary, however, there should not be any serious breakage. If you happen to come across a bug or if you have any suggestions, please take the time to drop me an e-mail to .

You will need Python (version >= 2.2) and a recent version of pygtk. Of course, you will need exim (version 3 or 4) installed as well. If exim is installed in a non-standard location, you will need to specify the paths in geximon preferences.


2. Installation

You may run the program by running geximon.py in the geximon directory. Moreover, as this program is packaged using distutils, you can easily install it by running ./setup.py install as the superuser. A script named geximon will be placed in your path which you can then use to invoke the program.


3. Configuration

geximon stores its configuration in the file .geximonrc in the user's home directory. Most settings as well as window positions are saved in this file in a human-readable format, however, there should be no need to edit it manually.

Log files can also be fetched from a remote server. In order to use this feature you will need to setup non-console authentication.


3.1. Privileges

geximon needs privileges to communicate with exim and to read its logs. The easiest solution is to run the program as the superuser.

If you are concerned about security, and prefer to run the program as an ordinary user, there are a few things you can do. Adding yourself to 'mail' and 'adm' groups might give enough priviledges to see the exim queue (try /usr/sbin/exim -bp) and to read the main logfile (/var/log/exim/mainlog). However, the process list will not be available (it needs to run /usr/sbin/exiwhat).

You can also use sudo (a program that allows controlled permission elevation) to great effect. You will need to configure sudo not to ask for a password for some commands. As the superuser, execute visudo to edit the sudo configuration file (/etc/sudoers), and add this line:

your_username ALL=(root) NOPASSWD: /usr/sbin/exim, /usr/sbin/exiwhat

[For more information about the file syntax, see the manual page of sudoers(5).]

Now, in geximon preferences, check the 'Use sudo' option. The changes should take effect immediately after applying the preferences window: the queue list and process list should start working. Note that you will still not be able to kill exim processes from geximon, to do that you must run geximon itself as the superuser.

Components that lack permissions to function properly should not interfere with others (e.g. if you can read the logfile, the log should be shown even if the queue can not be listed.)


4. Usage

The user interface should be fairly intuitive. One thing that might not be apparent at first glance is that messages with ID's highlighted in red are frozen, which means that they could not be delivered, but delivery will be attempted after some time passes.

Context menus are available in the queue list and the process list. You may hold down Ctrl to select multiple items and use Shift to select ranges; Ctrl+A marks all items.

If the confirmation and report popups annoy you, they can be turned off in preferences.

eximstats and exigrep are two quite useful utilities that are bundled with exim. They can be invoked through the 'Exim' menu. exigrep can also be invoked on fields of messages by using the context menu of the queue list.

You can choose the text wrapping mode in the context menu of textboxes.

If you are told a message is locked, it means that it is being processed by exim. To unlock it, you may want to use the process list to seek out the exim instance that is currently dealing with the message and send it a SIGTERM. The exim instance will terminate and the message should now be unlocked.

The plots have the same meanings as in the original eximon. The label colors correspond to the line colors. The number next to a label is the last value for that plot, visual scaling (if >1) is shown in brackets. If you see a single line only, it's probably because it simply overdraws the other lines.

When the process list is enabled, exiwhat is invoked regularly (the interval can be specified in preferences). Each run may produce a short but noticeable spike in CPU usage, so closing the window when it is not in use is a good idea.

geximon-0.7.7/doc/geximon.txt0000644000175000017500000001365310470723547015130 0ustar daviddavid gEximon documentation _________________________________________________________________ Table of Contents 1. [1]Introduction 2. [2]Installation 3. [3]Configuration 3.1. [4]Privileges 4. [5]Usage 1. Introduction geximon is a replacement for eximon, the exim server monitor. The original program is functional, but, to cite one of the exim mailing list members, "it could use a visit from one of the Learning Channel redecoration shows :-)". geximon is written in Python and is based on Gtk2. The source code is available under the GNU General Public Licence. This is a development version, so your mileage may vary, however, there should not be any serious breakage. If you happen to come across a bug or if you have any suggestions, please take the time to drop me an e-mail to <[6]david@planetwatson.co.uk>. You will need [7]Python (version >= 2.2) and a recent version of [8]pygtk. Of course, you will need [9]exim (version 3 or 4) installed as well. If exim is installed in a non-standard location, you will need to specify the paths in geximon preferences. _________________________________________________________________ 2. Installation You may run the program by running geximon.py in the geximon directory. Moreover, as this program is packaged using distutils, you can easily install it by running ./setup.py install as the superuser. A script named geximon will be placed in your path which you can then use to invoke the program. _________________________________________________________________ 3. Configuration geximon stores its configuration in the file .geximonrc in the user's home directory. Most settings as well as window positions are saved in this file in a human-readable format, however, there should be no need to edit it manually. Log files can also be fetched from a remote server. In order to use this feature you will need to setup non-console authentication. _________________________________________________________________ 3.1. Privileges geximon needs privileges to communicate with exim and to read its logs. The easiest solution is to run the program as the superuser. If you are concerned about security, and prefer to run the program as an ordinary user, there are a few things you can do. Adding yourself to 'mail' and 'adm' groups might give enough priviledges to see the exim queue (try /usr/sbin/exim -bp) and to read the main logfile (/var/log/exim/mainlog). However, the process list will not be available (it needs to run /usr/sbin/exiwhat). You can also use sudo (a program that allows controlled permission elevation) to great effect. You will need to configure sudo not to ask for a password for some commands. As the superuser, execute visudo to edit the sudo configuration file (/etc/sudoers), and add this line: your_username ALL=(root) NOPASSWD: /usr/sbin/exim, /usr/sbin/exiwhat [For more information about the file syntax, see the manual page of sudoers(5).] Now, in geximon preferences, check the 'Use sudo' option. The changes should take effect immediately after applying the preferences window: the queue list and process list should start working. Note that you will still not be able to kill exim processes from geximon, to do that you must run geximon itself as the superuser. Components that lack permissions to function properly should not interfere with others (e.g. if you can read the logfile, the log should be shown even if the queue can not be listed.) _________________________________________________________________ 4. Usage The user interface should be fairly intuitive. One thing that might not be apparent at first glance is that messages with ID's highlighted in red are frozen, which means that they could not be delivered, but delivery will be attempted after some time passes. Context menus are available in the queue list and the process list. You may hold down Ctrl to select multiple items and use Shift to select ranges; Ctrl+A marks all items. If the confirmation and report popups annoy you, they can be turned off in preferences. eximstats and exigrep are two quite useful utilities that are bundled with exim. They can be invoked through the 'Exim' menu. exigrep can also be invoked on fields of messages by using the context menu of the queue list. You can choose the text wrapping mode in the context menu of textboxes. If you are told a message is locked, it means that it is being processed by exim. To unlock it, you may want to use the process list to seek out the exim instance that is currently dealing with the message and send it a SIGTERM. The exim instance will terminate and the message should now be unlocked. The plots have the same meanings as in the original eximon. The label colors correspond to the line colors. The number next to a label is the last value for that plot, visual scaling (if >1) is shown in brackets. If you see a single line only, it's probably because it simply overdraws the other lines. When the process list is enabled, exiwhat is invoked regularly (the interval can be specified in preferences). Each run may produce a short but noticeable spike in CPU usage, so closing the window when it is not in use is a good idea. References 1. file://localhost/home/david/src/geximon-0.7.1/geximon-0.7.1/doc/geximon.html#AEN4 2. file://localhost/home/david/src/geximon-0.7.1/geximon-0.7.1/doc/geximon.html#AEN13 3. file://localhost/home/david/src/geximon-0.7.1/geximon-0.7.1/doc/geximon.html#AEN19 4. file://localhost/home/david/src/geximon-0.7.1/geximon-0.7.1/doc/geximon.html#AEN24 5. file://localhost/home/david/src/geximon-0.7.1/geximon-0.7.1/doc/geximon.html#AEN40 6. mailto:david@planetwatson.co.uk 7. http://www.python.org/ 8. http://www.daa.com.au/~james/software/pygtk/ 9. http://www.exim.org/ geximon-0.7.7/geximon/0000755000175000017500000000000011036202717013600 5ustar daviddavidgeximon-0.7.7/geximon/__init__.py0000644000175000017500000000000010470723547015711 0ustar daviddavidgeximon-0.7.7/geximon/exim.py0000644000175000017500000004056610773015745015141 0ustar daviddavid"""Interfacing with exim processes.""" import os, fcntl, select import threading __metaclass__ = type def get_output(path='', filename='', args='', use_sudo=False, use_ssh=False, hostname=''): """Run a command and return its output.""" stdouterr = get_pipe(path, filename, args, use_sudo, use_ssh, hostname) try: return stdouterr.read().strip() finally: stdouterr.close() def get_pipe(path='', filename='', args='', use_sudo=False, use_ssh=False, hostname=''): """Run a command and return its handle.""" cmd = (use_ssh and 'ssh %s '%hostname or '') + \ (use_sudo and 'sudo ' or '') + \ os.path.join(path, filename) if args: cmd += ' ' + args stdin, stdouterr = os.popen4(cmd) stdin.close() return stdouterr class LogWatcher: """Watch exim logs.""" def __init__(self, log_dir, mainlog_name, bin_dir, sudo, ssh, hostname, line_limit=500): self.line_limit = line_limit self.for_processing = [] self._valid = True self.bin_dir = bin_dir self.use_sudo = sudo self.use_ssh = ssh self.hostname = hostname self.open(log_dir, mainlog_name) def open(self, log_dir, mainlog_name): self.log_dir = log_dir self.mainlog_name = mainlog_name mainlog_path = os.path.join(log_dir, mainlog_name) try: # self.mainlog = open(mainlog_path) # self.mainlog_inode = os.fstat(self.mainlog.fileno()).st_ino # self._valid = True self.mainlog = get_pipe('/usr/bin','tail','-F %s'%mainlog_path,self.use_sudo, self.use_ssh, self.hostname) self.mainlog_fd = self.mainlog.fileno() fl = fcntl.fcntl(self.mainlog_fd, fcntl.F_GETFL) fcntl.fcntl(self.mainlog_fd, fcntl.F_SETFL, fl|os.O_NONBLOCK) self._valid = True except IOError: if self._valid: # XXX should show a popup self.unseen = \ [_("Error: could not open the exim log file at `%s`!") % mainlog_path] self._valid = False self.mainlog = None else: # self.unseen = self.mainlog.readlines()[-self.line_limit:] # self.unseen = [s[:-1] for s in self.unseen] self.unseen = [] def update(self): """Read the log file for new entries.""" mainlog_path = os.path.join(self.log_dir, self.mainlog_name) if self.mainlog: new_entries = [] if (select.select([self.mainlog_fd],[],[],0))[0]: buffer = os.read(self.mainlog_fd, 2000) for line in buffer.split("\n"): if line: new_entries.append(line) else: new_entries = [] self.open(self.log_dir, self.mainlog_name) return self.unseen += new_entries self.for_processing += new_entries def get_unseen(self): """Get unseen entries.""" unseen = self.unseen self.unseen = [] return unseen def get_for_processing(self): """Get list of lines that have not been processed yet.""" for_processing = self.for_processing self.for_processing = [] return for_processing def runExigrep(self, pattern, literal, all_logs): """Run exigrep with the provided pattern. `pattern` is a Perl regular expression, or a plain string - if `literal` is True. `all_logs` indicates if all logfiles (rather than just the latest one) should be scanned. """ filename = os.path.join(self.log_dir, all_logs and '*' or self.mainlog_name) pattern = "'" + pattern + "'" return get_output(self.bin_dir, 'exigrep', (literal and '-l' or '') + pattern + ' ' + filename, self.use_sudo, self.use_ssh, self.hostname) def runEximstats(self, args, all_logs): """Run eximstats. `args` is a string containing the arguments to pass to eximstats, all_logs indicates if all logfiles should be scanned. """ filename = os.path.join(self.log_dir, all_logs and '*' or self.mainlog_name) return get_output(self.bin_dir, 'eximstats', args + ' ' + filename, self.use_sudo, self.use_ssh, self.hostname) def getRejectlog(self): """Get the contents of the rejectlog.""" offset = self.mainlog_name.split('main') filename = 'reject'.join(offset) filename = os.path.join(self.log_dir, filename) try: f = open(filename) text = f.read() except IOError: text = '' # XXX should indicate an error return text def getPaniclog(self): """Get the contents of the paniclog.""" offset = self.mainlog_name.split('main') filename = 'panic'.join(offset) filename = os.path.join(self.log_dir, filename) try: f = open(filename) text = f.read() except IOError: text = '' # XXX should indicate an error return text class BackgroundJob: """On-demand background job manager. BackgroundJob starts a new background thread that goes to sleep. When it is woken up (by calling schedule_update), it performs some background processing (do_update) and goes to sleep again. Any calls to schedule_update while the background processing is active are ignored. The background thread can be stopped by calling stop. Subclasses must override do_update. """ def __init__(self): self._quit = False self._synch = threading.Event() self._thread = threading.Thread(target=self.bg_processing) self._thread.setDaemon(True) self._thread.start() def bg_processing(self): """Perform background processing on demand.""" while not self._quit: self._synch.wait() if self._quit: break self.do_update() self._synch.clear() def schedule_update(self): """Wake up the background thread.""" self._synch.set() def stop(self, timeout=1.0): """Stop background processing.""" self._quit = True self._synch.set() self._thread.join(timeout) def do_update(self): """Override this method to perform background jobs. It is called in a background thread. """ raise NotImplementedError("You need to override do_update in %s" % self.__class__.__name__) class QueueManager(BackgroundJob): """Exim queue manager. Queue manager calls exim and parses its output in a background thread to get the status of the exim queue. Once the status is available, the queue manager calls the registered callback. """ def __init__(self, callback, bin_dir, exim_binary, sudo, ssh, hostname): """Initialize and start the background thread. callback is a callable that takes one argument -- a mapping from message IDs to message objects. It will be called from the background thread. bin_dir is the path to exim. use_sudo indicates if sudo is to be used. """ BackgroundJob.__init__(self) self.callback = callback self.bin_dir = bin_dir self.exim_binary = exim_binary self.use_sudo = sudo self.use_ssh = ssh self.hostname = hostname self.queue_length = 0 def do_update(self): """Get the current exim queue in a separate thread.""" data = self.getEximOutput(['-bpr']) if (data.find('No such file or directory') != -1 or data.find(' not found') != -1 or data.find('exim: permission denied') != -1): # complain in case of problems m = Message() m.frozen, m.time, m.size, m.sender = True, "", "", "", # XXX should show a popup m.id, m.recipients = 'error', \ [_("Error invoking `exim -bpr`:\n") + data] self.messages = {'error': m} self.callback(self.messages) return data = data.splitlines() messages = {} recipient_list = False message = None for line in data: if not recipient_list: try: message = Message(status_line=line) except ValueError: pass # probably an odd status line, ignore it else: messages[message.id] = message recipient_list = True elif line: message.add_recipient(line) else: recipient_list = False self.queue_length = len(messages) self.callback(messages) def getEximOutput(self, args): """Invoke exim with arguments, return command output. args is a list of strings (the parameters to be passed). """ return get_output(self.bin_dir, self.exim_binary, " ".join(args), self.use_sudo, self.use_ssh, self.hostname) def checkOutput(self, output, expected, msg_count, action_name): """Check if a string has `expected` as a substring on each line. Returns a tuple: (no_errors, report_string). """ lines = output.splitlines() errors = filter(lambda s: s.find(expected) == -1, lines) if not errors: words = msg_count > 1 and 'messages have' or 'message has' success_report = _("%d %s been %s.") \ % (msg_count, words, action_name) return (True, success_report) else: return (False, "\n".join(errors)) # -- general actions -- def runQueue(self): """Run the queue now.""" # XXX a little dirty, but will do for now cmd = '' if self.use_ssh: cmd = cmd + 'ssh %s ' % self.hostname if self.use_sudo: cmd = cmd + 'sudo ' cmd = cmd + os.path.join(self.bin_dir, self.exim_binary) + ' -q &' os.system(cmd) return _("Spawning a queue runner in the background.") def getConfiguration(self): """Get all exim configuration options.""" return self.getEximOutput(['-bP']) # -- message actions -- # methods must return a tuple: (action_successful, message) def getMessageBody(self, id): """Get the body of the message with the given ID.""" return (True, self.getEximOutput(['-Mvb', id])) def getMessageHeaders(self, id): """Get the headers of the message with the given ID.""" return (True, self.getEximOutput(['-Mvh', id])) def getMessageAll(self, id): """Get the entire message with the given ID.""" return (True, self.getEximOutput(['-Mvc', id])) def getMessageLog(self, id): """Get the log of the message with the given ID.""" return (True, self.getEximOutput(['-Mvl', id])) def removeMessages(self, ids): """Remove messages with given IDs.""" output = self.getEximOutput(['-Mrm'] + ids) return self.checkOutput(output, "has been removed", len(ids), _("removed")) def freezeMessages(self, ids): """Freeze messages with given IDs.""" output = self.getEximOutput(['-Mf'] + ids) return self.checkOutput(output, "is now frozen", len(ids), _("frozen")) def thawMessages(self, ids): """Unfreeze messages with given IDs.""" output = self.getEximOutput(['-Mt'] + ids) return self.checkOutput(output, "is no longer frozen", len(ids), _("thawed")) def deliverMessages(self, ids): """Force the delivery of messages with given IDs.""" return (True, self.getEximOutput(['-M'] + ids)) def giveUpMessages(self, ids): """Give up trying to deliver messages with given IDs.""" return (True, self.getEximOutput(['-Mg'] + ids)) def addRecipients(self, id, recipients): """Add more recipients to a message with a given ID.""" output = self.getEximOutput(['-Mar', id] + recipients.split()) return self.checkOutput(output, "has been modified", 1, _("modified")) def editSender(self, id, sender): """Change the sender of a message with a given ID.""" output = self.getEximOutput(['-Mes', id, sender]) return self.checkOutput(output, "has been modified", 1, _("modified")) def markAllDelivered(self, ids): """Mark all recipients of messages with given IDs as delivered.""" output = self.getEximOutput(['-Mmad'] + ids) return self.checkOutput(output, "has been modified", len(ids), _("marked as delivered")) class Message: """A queued message. >>> line = '22h 1.2K 1AttSk-0002qB-00 <> *** frozen ***' >>> m = Message(line) >>> m.id, m.frozen, m.time, m.sender, m.size, m.recipients ('1AttSk-0002qB-00', True, '22h', '<>', '1.2K', []) >>> m.add_recipient(' foobar\\n') >>> m.recipients ['foobar'] """ def __init__(self, status_line=None): """Extract information from a message status line returned by exim.""" if not status_line is None: parts = status_line.split() if len(parts) < 4: raise ValueError(_("Invalid status line: ") + status_line) self.id = parts[2] self.frozen = "frozen" in parts self.time = parts[0] self.size = parts[1] self.sender = parts[3] self.recipients = ['0','sent,','0','unsent:'] def add_recipient(self, line): """Add a recipient to the message.""" recipient = line.strip() if recipient[0] == 'D' and recipient[1] == ' ': self.recipients[0] = str(int(self.recipients[0]) + 1) else: self.recipients.append(recipient) self.recipients[2] = str(int(self.recipients[2]) + 1) self.recipients[0] = str(int(self.recipients[0]) + 1) def __eq__(self, other): return (self.id == other.id and self.frozen == other.frozen and self.time == other.time and self.size == other.size and self.sender == other.sender and self.recipients == other.recipients) def __ne__(self, other): return not self.__eq__(other) class ProcessManager(BackgroundJob): """Exim process manager. Process manager calls exiwhat(8) and parses its output in a background thread to get the status of exim processes. Once the status is available, the process manager calls the registered callback. """ def __init__(self, callback, bin_dir, use_sudo, use_ssh, hostname): """Start the background thread. callback is a callable that takes one argument -- a mapping from process IDs to status messages. It will be called from the background thread. bin_dir is the path to exim binaries. use_sudo indicates if sudo is to be used. """ BackgroundJob.__init__(self) self.callback = callback self.bin_dir = bin_dir self.use_sudo = use_sudo self.use_ssh = use_ssh self.hostname = hostname def do_update(self): """Collect data from exiwhat in a separate thread.""" data = get_output(self.bin_dir, 'exiwhat', use_sudo=self.use_sudo, use_ssh=self.use_ssh, hostname=self.hostname) processes = {} if (data.find('Permission denied') != -1 or data.find('Operation not permitted') != -1): # XXX need to be more verbose status = _("Permission problems!") elif data.find('No exim process data') != -1: status = _("No exim processes are currently running.") elif data == '': status = _("No output from exiwhat!") else: status = "" for line in data.splitlines(): try: id, info = line.split(None, 1) id = int(id) processes[id] = info.strip() except ValueError: # probably trying to parse an error message status = _("Error processing exiwhat output line: ") + line if not status: process_word = (len(processes) > 1 and _("processes") or _("process")) status = _("%d exim %s.") % (len(processes), process_word) self.callback(processes, status) geximon-0.7.7/geximon/geximon.py0000644000175000017500000003055011036202717015623 0ustar daviddavid""" Copyright (C) 2004 Gintautas Miliauskas , Programmers of Vilnius 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ VERSION = "0.7.7" import gettext gettext.install('geximon') import sys try: import gtk except RuntimeError, e: if str(e) == "could not open display": print "Could not connect to an X display" sys.exit() import gobject from exim import LogWatcher, QueueManager from gtkhelpers import AlertDialog from gtkhelpers import framed, scrolled, show_help from widgets import LogWidget, ProcessWidget, QueueWidget from widgets import PopupWindow, ExigrepDialog, EximstatsDialog from preferences import Preferences, PreferencesDialog from plotter import Plotter class GEximonWindow(gtk.Window): """The main geximon window.""" def __init__(self): gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) self.set_title('gEximon') self.connect('delete-event', self.quit) self.prefs = prefs = Preferences() prefs.load() prefs.subscribe(self.apply_prefs) self.logwatcher = LogWatcher(prefs.log_dir, prefs.mainlog_name, prefs.bin_dir, prefs.use_sudo, prefs.use_ssh, prefs.hostname) # callback is None, it will be specified by QueueWidget self.queue_mgr = QueueManager(None, prefs.bin_dir, prefs.exim_binary, prefs.use_sudo, prefs.use_ssh, prefs.hostname) self._setUpIcons() # create menu bar self._setUpMenu() # create status bar self.statusbar = gtk.Statusbar() self.statusbar.push(0, '') # set up widgets self.plotter = Plotter(self.logwatcher, self.queue_mgr, prefs) self.log_widget = LogWidget(self.logwatcher, prefs) self.queue_widget = QueueWidget(self, self.logwatcher, self.queue_mgr, prefs) self.process_window = ProcessWindow(prefs) def processWindowClosed(*ignored_arguments): self.prefs.show_process_list = False self.prefs.notify() return True # prevent the window from getting destroyed self.process_window.connect('delete-event', processWindowClosed) self._layOutWidgets() prefs.notify() def _setUpIcons(self): try: icon16x16 = gtk.gdk.pixbuf_new_from_file( '/usr/share/pixmaps/geximon-16x16.xpm') icon32x32 = gtk.gdk.pixbuf_new_from_file( '/usr/share/pixmaps/geximon-32x32.xpm') except gobject.GError: pass else: gtk.window_set_default_icon_list(icon32x32, icon16x16) def _setUpMenu(self): """Set up the main menubar.""" def runQueue(*ignored_arguments): self.queue_mgr.runQueue() if self.prefs.report_success: AlertDialog(self, _("A queue runner has been started."), _("A new exim instance has been started in the background." " It will process the queue and then terminate.")) def runExigrep(*ignored_arguments): dialog = ExigrepDialog(self) dialog.go(self.logwatcher) def runEximstats(*ignored_arguments): dialog = EximstatsDialog(self) dialog.go(self.logwatcher) def getRejectlog(*ignored_arguments): text = self.logwatcher.getRejectlog() PopupWindow(_("Reject log"), text).show_all() def getPaniclog(*ignored_arguments): text = self.logwatcher.getPaniclog() PopupWindow(_("Panic log"), text).show_all() def getConfig(*ignored_arguments): text = self.queue_mgr.getConfiguration() PopupWindow(_("Exim configuration"), text).show_all() def scrollLogToggled(menuitem): """Automatically scroll the log on new data.""" self.prefs.track_log = menuitem.get_active() self.prefs.notify() def processDisplayToggled(menuitem): """Show or hide the process list window.""" self.prefs.show_process_list = menuitem.get_active() self.prefs.notify() def plotDisplayToggled(menuitem): """Show or hide the plot area.""" self.prefs.show_plotter = menuitem.get_active() self.prefs.notify() def statusbarDisplayToggled(menuitem): """Show or hide the statusbar.""" self.prefs.show_statusbar = menuitem.get_active() self.prefs.notify() def helpContents(menuitem): """Show the help contents.""" show_help() ui_string = """ """ ag = gtk.ActionGroup('WindowActions') actions = [ ('GeximonMenu', None, '_Geximon'), ('Preferences', gtk.STOCK_PREFERENCES, '_Preferences', 'P', 'Open the preferences dialog', self.editPreferences), ('Quit', gtk.STOCK_QUIT, '_Quit', 'Q', 'Quit application', self.quit), ('EximMenu', None, '_Exim'), ('Spawn Queue Runner', None, 'Spawn _Queue Runner', 'R', 'Spawn Queue Runner', runQueue), ('Exigrep', None, 'Exi_grep', None, 'Run Exigrep', runExigrep), ('Eximstats', None, 'Exim_stats', None, 'Run Eximstats', runEximstats), ('Rejectlog', None, 'See _Rejectlog', None, 'See Rejectlog', getRejectlog), ('Paniclog', None, 'See _Paniclog', None, 'See Paniclog', getPaniclog), ('Get_Configuration', None, 'Get _Configuration', None, 'Get Configuration', getConfig), ('ViewMenu', None, '_View'), ('HelpMenu', None, '_Help'), ('Contents', None, '_Contents', None, 'Show help contents', helpContents), ('About', None, '_About', None, 'Show about dialog', self.about), ] actionstoggle = [ ('Plots', None, '_Plots', 'G', 'Toggle plot view', plotDisplayToggled), ('Statusbar', None, '_Statusbar', None, 'Toggle statusbar view', statusbarDisplayToggled), ('ProcessList', None, 'P_rocess List', 'L', 'Toggle process list view', processDisplayToggled), ('Scroll', None, 'Scroll _log on new data', 'S', 'Toggle scroll log', scrollLogToggled), ] ag.add_actions(actions) ag.add_toggle_actions(actionstoggle) self.ui = gtk.UIManager() self.ui.insert_action_group(ag, 0) self.ui.add_ui_from_string(ui_string) self.add_accel_group(self.ui.get_accel_group()) self.show_plotter_menu_item = ag.get_action('Plots') self.show_statusbar_menu_item = ag.get_action('Statusbar') self.process_list_menu_item = ag.get_action('ProcessList') self.track_log_menu_item = ag.get_action('Scroll') def _layOutWidgets(self): self.pane = gtk.VPaned() self.pane.pack1(framed(scrolled(self.log_widget)), resize=True) self.pane.pack2(framed(scrolled(self.queue_widget)), resize=True) self.vbox = gtk.VBox() self.vbox.set_border_width(2) self.vbox.pack_start(framed(self.plotter), expand=False) self.vbox.pack_end(self.pane) self.vbox2 = gtk.VBox() self.vbox2.pack_start(self.ui.get_widget('/menubar'), expand=False) self.vbox2.pack_start(self.vbox, expand=True) self.vbox2.pack_end(self.statusbar, expand=False) self.add(self.vbox2) if self.prefs.remember_sizes: self.set_default_size( self.prefs.window_width, self.prefs.window_height) self.pane.set_position(self.prefs.divider_position) else: self.set_default_size(550, 600) def quit(self, *ignored_arguments): self.save_preferences() # it would be nice if the following were called automatically self.process_window.process_widget.cleanup() gtk.main_quit() return False def save_preferences(self): self.prefs.window_width, self.prefs.window_height = self.get_size() self.prefs.divider_position = self.pane.get_position() self.prefs.wrap_log = self.log_widget.get_wrap_mode() self.prefs.save() def about(self, *ignored_arguments): """Show the about dialog.""" about_dialog = AboutDialog(self) about_dialog.go() def editPreferences(self, *ignored_arguments): """Show the preferences dialog.""" pref_dialog = PreferencesDialog(self, self.prefs) pref_dialog.show_all() response = pref_dialog.run() if response == gtk.RESPONSE_OK: pref_dialog.apply(self.prefs) pref_dialog.hide() def apply_prefs(self, prefs): self.process_list_menu_item.set_active(prefs.show_process_list) self.show_plotter_menu_item.set_active(prefs.show_plotter) self.show_statusbar_menu_item.set_active(prefs.show_statusbar) if prefs.show_statusbar: self.statusbar.show_all() else: self.statusbar.hide() self.track_log_menu_item.set_active(prefs.track_log) class ProcessWindow(gtk.Window): """A window that contains the exim process list.""" def __init__(self, prefs): gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) self.set_default_size(500, 200) self.set_title(_("Exim processes")) self.statusbar = gtk.Statusbar() self.statusbar.push(0, '') self.process_widget = ProcessWidget(self.statusbar, prefs) vbox = gtk.VBox() vbox.pack_start(scrolled(self.process_widget)) vbox.pack_end(self.statusbar, expand=False) self.add(framed(vbox)) prefs.subscribe(self.apply_prefs) def apply_prefs(self, prefs): if prefs.show_process_list: self.process_widget.update() self.show_all() else: self.hide() class AboutDialog(gtk.Dialog): """The About dialog.""" def __init__(self, main_window): gtk.Dialog.__init__(self, _("About gEximon"), main_window, flags=(gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT), buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK)) self.set_resizable(False) self.set_border_width(12) self.vbox.set_spacing(18) text = 'gEximon v%s' % VERSION title = gtk.Label(text) title.set_property('use-markup', True) self.vbox.add(title) text = _("A GNOME monitor for the exim mail transport agent.") about = gtk.Label(text) self.vbox.add(about) text = ("" + _("Author") + ": " + "Gintautas Miliauskas <gintas@pov.lt>\n" + "Programmers of Vilnius, 2004\n" + "Now maintained by David Watson <david@planetwatson.co.uk>") author = gtk.Label(text) author.set_property('use-markup', True) self.vbox.add(author) def go(self): self.show_all() self.run() self.destroy() def main(): mainwin = GEximonWindow() mainwin.show_all() # XXX these should be moved somewhere else if not mainwin.prefs.show_plotter: mainwin.plotter.set_visible(False) if not mainwin.prefs.show_statusbar: mainwin.statusbar.hide() gtk.gdk.threads_init() gtk.main() if __name__ == '__main__': main() geximon-0.7.7/geximon/gtkhelpers.py0000644000175000017500000002170110470723547016335 0ustar daviddavid"""Convenience classes for GTK.""" import sys try: import gtk except RuntimeError, e: if str(e) == "could not open display": print "Could not connect to an X display" sys.exit() import os import gobject class WrappedTextView(gtk.TextView): """A custom TextView widget. It is read-only and wrapping can be controlled through a context menu. """ def __init__(self): gtk.TextView.__init__(self) self.set_editable(False) self.connect('populate-popup', self._populate_popup) def _populate_popup(self, textview, menu): wrapping_submenu = gtk.Menu() menuitem1 = gtk.RadioMenuItem(label=_("_None"), group=None) menuitem1.connect('activate', self._set_wrap_mode, gtk.WRAP_NONE) wrapping_submenu.add(menuitem1) menuitem2 = gtk.RadioMenuItem(label=_("_On characters"), group=menuitem1) menuitem2.connect('activate', self._set_wrap_mode, gtk.WRAP_CHAR) wrapping_submenu.add(menuitem2) menuitem3 = gtk.RadioMenuItem(label=_("_On words"), group=menuitem1) menuitem3.connect('activate', self._set_wrap_mode, gtk.WRAP_WORD) wrapping_submenu.add(menuitem3) mode = textview.get_wrap_mode() if mode == gtk.WRAP_NONE: menuitem1.set_active(True) elif mode == gtk.WRAP_CHAR: menuitem2.set_active(True) elif mode == gtk.WRAP_WORD: menuitem3.set_active(True) menuitem = gtk.MenuItem(_("_Wrapping")) menuitem.set_submenu(wrapping_submenu) menuitem.show_all() separator = gtk.SeparatorMenuItem() separator.show_all() menu.prepend(separator) menu.prepend(menuitem) return True def _set_wrap_mode(self, menuitem, wrap_mode): self.set_wrap_mode(wrap_mode) class PopupWindow(gtk.Window): """A popup window. Contains a TextView to show some text.""" def __init__(self, title, text): gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) self.set_title(title) textview = WrappedTextView() textview.set_editable(False) buffer = self.buffer = textview.get_buffer() buffer.set_text(text) monospace = buffer.create_tag() monospace.set_property('family', 'Monospace') buffer.apply_tag(monospace, buffer.get_start_iter(), buffer.get_end_iter()) vbox = gtk.VBox() self.createMenuBar() vbox.pack_start(self.menubar, expand=False) vbox.pack_end(framed(scrolled(textview))) self.add(vbox) self.set_default_size(600, 400) def createMenuBar(self): ui_string = """ """ ag = gtk.ActionGroup('WindowActions') actions = [ ('FileMenu', None, '_File'), ('SaveAs', gtk.STOCK_SAVE_AS, '_Save As', 'S', 'Save As', self.saveToFile), ('Close', gtk.STOCK_CLOSE, '_Close', 'W', 'Close', lambda *args: self.destroy()), ] ag.add_actions(actions) self.ui = gtk.UIManager() self.ui.insert_action_group(ag, 0) self.ui.add_ui_from_string(ui_string) self.add_accel_group(self.ui.get_accel_group()) self.menubar = self.ui.get_widget('/menubar') def saveToFile(self, *args): """Save the contents of the window to a text file.""" filesel = gtk.FileSelection() filesel.run() filename = filesel.get_filename() filesel.destroy() if os.path.exists(filename): dialog = AlertDialog(self, _("The file with that name already exists." "If you continue, the contents of the file will " "be overwritten."), query=True) response = dialog.ask() if not response: return bounds = self.buffer.get_bounds() text = self.buffer.get_text(*bounds) try: f = open(filename, 'w') f.truncate(0) f.write(text) f.close() except IOError, e: dialog = AlertDialog(self, _("An error has occured."), _("The file at `%s` could not be written.\nError: %s") % (filename, e.strerror), error=True) dialog.ask() else: dialog = AlertDialog(self, _("Text saved successfully."), _("The data was successfully saved to `%s`.") % filename) dialog.ask() class EntryDialog(gtk.Dialog): """An entry dialog. Shows a modal dialog with a label and an Entry field. """ def __init__(self, main_window, title, label_text): gtk.Dialog.__init__(self, title=title, parent=main_window, flags=(gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT), buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) self.set_resizable(False) self.set_border_width(12) self.vbox.set_spacing(12) label = gtk.Label(label_text) self.entry = gtk.Entry() self.entry.set_property('activates-default', True) label.set_mnemonic_widget(self.entry) hbox = gtk.HBox() hbox.set_border_width(6) hbox.set_spacing(12) hbox.pack_start(label) hbox.pack_end(self.entry) self.vbox.add(hbox) self.ok_button = self.action_area.get_children()[0] self.ok_button.set_flags(gtk.CAN_DEFAULT) self.ok_button.grab_default() def get_input(self): """Return user input. Return a string: the text the user has input into the Entry. If the user chooses Cancel, return an empty string. """ self.show_all() response = self.run() text = self.entry.get_text() self.destroy() if response == gtk.RESPONSE_OK: return text else: return "" class AlertDialog(gtk.MessageDialog): """An alert dialog. Asks the user for verification of some action or provides information. """ def __init__(self, main_window, header, text, query=False, error=False): if query: buttons = gtk.BUTTONS_OK_CANCEL icon = gtk.MESSAGE_WARNING else: buttons = gtk.BUTTONS_OK icon = error and gtk.MESSAGE_ERROR or gtk.MESSAGE_INFO dlg_text = ('%s\n\n%s' % (header, text)) gtk.MessageDialog.__init__(self, main_window, (gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT), icon, buttons, dlg_text) self.label.set_property('use-markup', True) def ask(self): self.show_all() response = self.run() self.destroy() return (response == gtk.RESPONSE_OK) class Timer: """A higher-level timer. The GTK timers are rather dumb - they cannot be stopped externally or their intervals changed. This class allows to do just that. """ class TimerBackend: """A timer that can be stopped.""" def __init__(self, interval, callback, paused=False): self._callback = callback self.cancelled = False self.paused = paused gobject.timeout_add(interval, self.hit) def hit(self): if not (self.paused or self.cancelled): self._callback() return not self.cancelled def __init__(self, interval, callback): self._interval = interval self._callback = callback self._timer = self.TimerBackend(interval, callback) self._paused = False def update_interval(self, new_interval): if self._interval != new_interval: self._timer.cancelled = True self._interval = new_interval self._timer = self.TimerBackend( new_interval, self._callback, self._paused) def set_paused(self, paused): self._paused = paused self._timer.paused = paused def show_help(): """Open a help window.""" import os helpfile = os.path.abspath('../doc/geximon.html') if not os.path.exists(helpfile): helpfile = '/usr/share/doc/geximon/geximon.html' (os.system('gnome-moz-remote file://%s' % helpfile) == 0) or \ (os.system('mozilla file://%s' % helpfile) == 0) or \ (os.system('see %s &' % helpfile) == 0) # decorators def scrolled(widget): """Wrap widget in a gtk.ScrolledWindow and return the scrolled window.""" scrolled_window = gtk.ScrolledWindow() scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scrolled_window.add(widget) return scrolled_window def framed(widget): """Wrap widget in a gtk.Frame and return the frame.""" frame = gtk.Frame() frame.set_shadow_type(gtk.SHADOW_IN) frame.add(widget) return frame geximon-0.7.7/geximon/plotter.py0000644000175000017500000001507110470723547015661 0ustar daviddavid"""Plotting of exim stats.""" import sys try: import gtk except RuntimeError, e: if str(e) == "could not open display": print "Could not connect to an X display" sys.exit() import gobject import re from gtkhelpers import Timer KEEP_HISTORY = 2000 DELTA = 0.0000001 class Plotter(gtk.DrawingArea): """A widget that contains multiple color-coded plots.""" config = [# label, regexp, color ('in', ' <= ', 'yellow'), ('out', ' => ', 'red'), ('local', ' => .+ R=local', 'blue'), ('smtp', ' => .+ T=[^ ]*smtp', 'green'), ('queue', None, 'white'), ] def __init__(self, logwatcher, queue_mgr, prefs): gtk.DrawingArea.__init__(self) self._logwatcher = logwatcher self._queue_mgr = queue_mgr self.set_size_request(-1, prefs.plot_area_height) self.visible = True self.interval = prefs.plotting_interval prefs.subscribe(self.apply_prefs) self.pangolayout = self.create_pango_layout("") self.gc = None # can't create as the window isn't realized yet colormap = self.get_colormap() self.background_color = colormap.alloc_color('black') self.guide_colors = [colormap.alloc_color('#AAAAAA'), colormap.alloc_color('#555555'), colormap.alloc_color('#222222')] self.plots = [] for label, regex, color in self.config: compiled_regex = regex and re.compile(regex) or None mapped_color = colormap.alloc_color(color) history = [] self.plots.append((label, compiled_regex, mapped_color, history)) self.connect('expose-event', self._redraw) self.timer = Timer(self.interval, self.update) def _redraw(self, area, event): """Redraw the plots. Must not be invoked directly, because double-buffering won't work; queue_redraw() should be used instead. """ geometry = self.window.get_geometry() width, height = geometry[2], geometry[3] if not self.gc: self.gc = self.window.new_gc() # clear drawing area self.gc.set_foreground(self.background_color) self.window.draw_rectangle(self.gc, True, 0, 0, width, height) start_x = 18 point_count = width - start_x plot_height = height - 20 label_offset = start_x + 10 self._draw_guides(width, plot_height, start_x) for label, compiled_regex, mapped_color, history in self.plots: scale = self.get_scale(history) self.gc.set_foreground(mapped_color) if len(history) > 1: # transform data points to screen coordinates points = enumerate(history[-point_count:]) coord_list = [(start_x + index, int(plot_height * (1 - value*scale))) for index, value in points] self.window.draw_lines(self.gc, coord_list) # draw label if label_offset < width: suffix = ": %.1f" % (history and history[-1] or 0) if abs(scale - 0.1) > DELTA: suffix = (" (%dx)" % (0.1/scale)) + suffix self.pangolayout.set_text(label + suffix) self.window.draw_layout(self.gc, label_offset, plot_height, self.pangolayout) label_offset += len(label + suffix) * 9 # XXX text width return True def _draw_guides(self, width, plot_height, start_x): """Draw guide lines.""" # draw main boundaries self.gc.set_foreground(self.guide_colors[0]) self.window.draw_line(self.gc, start_x, 0, width, 0) self.window.draw_line(self.gc, start_x, plot_height, width, plot_height) # draw numbers # XXX hardcoding font sizes number_offset = 0 self.pangolayout.set_text("10") self.window.draw_layout(self.gc, number_offset, -2, self.pangolayout) self.pangolayout.set_text(" 5") self.window.draw_layout(self.gc, number_offset, plot_height / 2 - 8, self.pangolayout) self.pangolayout.set_text(" 0") self.window.draw_layout(self.gc, number_offset, plot_height - 16, self.pangolayout) # draw a nice line indicating 5 self.gc.set_foreground(self.guide_colors[1]) self.window.draw_line(self.gc, start_x, plot_height / 2, width, plot_height / 2) # draw minor lines self.gc.set_foreground(self.guide_colors[2]) for v in [1, 2, 3, 4, 6, 7, 8, 9]: y = int(plot_height * (v / 10.0)) self.window.draw_line(self.gc, start_x, y, width, y) def get_scale(self, list): """Calculate a scaling value for a list of floats. Returns a floating point value - a multiplier to normalize the data. """ if len(list) < 2 or max(list) < 10: return 0.1 largest = max(list) scale = 1.0 while largest*scale > 1: scale /= 10 return scale def update(self, *ignored_arguments): """Update plot data.""" new_loglines = self._logwatcher.get_for_processing() for label, compiled_regex, mapped_color, history in self.plots: if label == 'queue': norm_count = float(self._queue_mgr.queue_length) else: count = self.count_matches(compiled_regex, new_loglines) norm_count = count * (1000.0 / self.interval) # normalize history.append(norm_count) if len(history) > KEEP_HISTORY: history.pop(0) self.queue_draw() def count_matches(self, regex, lines): count = 0 for line in lines: if regex.search(line): count += 1 return count def apply_prefs(self, prefs): self.set_size_request(-1, prefs.plot_area_height) self.interval = prefs.plotting_interval self.timer.update_interval(self.interval) if prefs.show_plotter != self.visible: self.set_visible(prefs.show_plotter) self.queue_draw() def set_visible(self, visible): self.visible = visible parent = self.get_parent() # hide the frame as well if visible: parent.show_all() else: parent.hide() geximon-0.7.7/geximon/preferences.py0000644000175000017500000003121610773030266016463 0ustar daviddavid"""Geximon application preferences.""" import os import sys try: import gtk except RuntimeError, e: if str(e) == "could not open display": print "Could not connect to an X display" sys.exit() import gobject import ConfigParser __metaclass__ = type class Preferences: track_log = True wrap_log = gtk.WRAP_CHAR show_process_list = False show_plotter = True show_statusbar = True log_dir = '/var/log/exim' bin_dir = '/usr/sbin' exim_binary = 'exim' mainlog_name = 'mainlog' use_sudo = False use_ssh = False hostname = '' log_interval = 200 # this is not accessible in GUI queue_interval = 2000 process_interval = 5000 plotting_interval = 1000 remember_sizes = True window_width = 600 window_height = 500 plot_area_height = 80 divider_position = 200 confirm_actions = True report_success = True show_delivery = False def __init__(self): self.config_filename = os.path.expanduser('~/.geximonrc') self.autodetect_version() self._callbacks = [] def autodetect_version(self): """Check if using exim4 rather than v3, change paths accordingly.""" if os.path.exists('/var/log/exim4'): self.log_dir = '/var/log/exim4' if os.path.exists('/usr/sbin/exim4'): self.exim_binary = 'exim4' if os.path.exists(os.path.join(self.log_dir, 'main.log')): self.mainlog_name = 'main.log' def load(self): """Load the preferences from a file.""" parser = ConfigParser.ConfigParser() try: parser.read([self.config_filename]) except ConfigParser.ParsingError, e: print >> sys.stderr, e return def load_value(section, attr, getter): try: setattr(self, attr, getter(section, attr)) except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): pass except ValueError, e: print >> sys.stderr, _("Invalid setting for %r in %s:") \ % (attr, self.config_filename) print >> sys.stderr, e load_bool = lambda s, attr: load_value(s, attr, parser.getboolean) load_int = lambda s, attr: load_value(s, attr, parser.getint) load_str = lambda s, attr: load_value(s, attr, parser.get) load_bool('display', 'show_plotter') load_bool('display', 'show_statusbar') load_bool('display', 'track_log') load_str('display', 'wrap_log') self.wrap_log = gtk.WRAP_CHAR load_bool('display', 'show_process_list') load_str('paths', 'log_dir') load_str('paths', 'bin_dir') load_str('paths', 'exim_binary') load_str('paths', 'mainlog_name') load_bool('paths', 'use_sudo') load_bool('paths', 'use_ssh') load_str('paths', 'hostname') load_int('timers', 'log_interval') load_int('timers', 'queue_interval') load_int('timers', 'process_interval') load_int('timers', 'plotting_interval') load_bool('dimensions', 'remember_sizes') load_int('dimensions', 'window_width') load_int('dimensions', 'window_height') load_int('dimensions', 'plot_area_height') load_int('dimensions', 'divider_position') load_bool('popups', 'confirm_actions') load_bool('popups', 'report_success') load_bool('popups', 'show_delivery') def save(self): """Save the preferences to a file.""" parser = ConfigParser.ConfigParser() parser.add_section('display') parser.set('display', 'show_plotter', self.show_plotter) parser.set('display', 'show_statusbar', self.show_statusbar) parser.set('display', 'track_log', self.track_log) parser.set('display', 'show_process_list', self.show_process_list) parser.set('display', 'wrap_log', self.wrap_log) parser.add_section('paths') parser.set('paths', 'log_dir', self.log_dir) parser.set('paths', 'bin_dir', self.bin_dir) parser.set('paths', 'exim_binary', self.exim_binary) parser.set('paths', 'mainlog_name', self.mainlog_name) parser.set('paths', 'use_sudo', self.use_sudo) parser.set('paths', 'use_ssh', self.use_ssh) parser.set('paths', 'hostname', self.hostname) parser.add_section('timers') parser.set('timers', 'log_interval', self.log_interval) parser.set('timers', 'queue_interval', self.queue_interval) parser.set('timers', 'process_interval', self.process_interval) parser.set('timers', 'plotting_interval', self.plotting_interval) parser.add_section('dimensions') parser.set('dimensions', 'remember_sizes', self.remember_sizes) parser.set('dimensions', 'window_width', self.window_width) parser.set('dimensions', 'window_height', self.window_height) parser.set('dimensions', 'plot_area_height', self.plot_area_height) parser.set('dimensions', 'divider_position', self.divider_position) parser.add_section('popups') parser.set('popups', 'confirm_actions', self.confirm_actions) parser.set('popups', 'report_success', self.report_success) parser.set('popups', 'show_delivery', self.show_delivery) file = open(self.config_filename, 'w') try: parser.write(file) finally: file.close() def subscribe(self, callback): """Subscribe a callback for change notification.""" self._callbacks.append(callback) def notify(self): """Notify all subscribers that preferences have changed.""" for callback in self._callbacks: callback(self) class PreferencesDialog(gtk.Dialog): def __init__(self, main_window, prefs): gtk.Dialog.__init__(self, title=_("Preferences"), parent=main_window, flags=(gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT), buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) self._main_window = main_window self.set_resizable(False) self.set_border_width(12) self.vbox.set_spacing(18) self._setup_path_settings(prefs) self._setup_timer_settings(prefs) self._setup_popup_settings(prefs) self._setup_ui_settings(prefs) ok_button = self.action_area.get_children()[0] ok_button.set_flags(gtk.CAN_DEFAULT) ok_button.grab_default() def _setup_path_settings(self, prefs): paths = self.newStyleFrame(_("Paths")) pathgroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) def packedPathEntry(description, value): label = gtk.Label(description) label.set_alignment(0.0, 0.5) label.set_use_underline(True) pathgroup.add_widget(label) entry = gtk.Entry() entry.set_text(value) entry.set_size_request(200, -1) entry.set_property('activates-default', True) label.set_mnemonic_widget(entry) hbox = gtk.HBox(spacing=6) hbox.pack_start(label) hbox.pack_end(entry) paths.pack_start(hbox) return entry self._log_dir= packedPathEntry( _("Exim _log directory:"), prefs.log_dir) self._mainlog_name = packedPathEntry( _("_Name of main log:"), prefs.mainlog_name) self._bin_dir = packedPathEntry( _("Path to exim _binaries:"), prefs.bin_dir) self._exim_binary = packedPathEntry( _("_Name of exim binary:"), prefs.exim_binary) self._use_sudo = gtk.CheckButton(_("Use _sudo")) if prefs.use_sudo: self._use_sudo.set_active(1) paths.pack_start(self._use_sudo) self._use_ssh = gtk.CheckButton(_("Use _ssh")) if prefs.use_ssh: self._use_ssh.set_active(1) paths.pack_start(self._use_ssh) self._hostname = packedPathEntry(_("Remote Hostname"), prefs.hostname) def _setup_timer_settings(self, prefs): timers = self.newStyleFrame(_("Update intervals")) timergroup = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL) def packedTimerSpin(description, value, min, max, step): label = gtk.Label(description) label.set_alignment(0.0, 0.5) timergroup.add_widget(label) adj = gtk.Adjustment(float(value)/1000, min, max, step) spinner = gtk.SpinButton(adj, digits=1) spinner.set_size_request(60, -1) spinner.set_property('activates-default', True) hbox = gtk.HBox(spacing=6) hbox.pack_start(label, expand=False) hbox.pack_start(spinner, expand=False) hbox.pack_start(gtk.Label(_("seconds")), expand=False) timers.pack_start(hbox) return spinner self._queue_interval = packedTimerSpin( _("Queue display:"), prefs.queue_interval, 0.5, 60, 0.1) self._process_interval = packedTimerSpin( _("Process display:"), prefs.process_interval, 2, 60, 1) self._plotting_interval = packedTimerSpin( _("Plots:"), prefs.plotting_interval, 0.5, 60, 0.5) def _setup_ui_settings(self, prefs): ui = self.newStyleFrame(_("User interface")) self._remember_sizes = gtk.CheckButton( _("Remember window size between sessions.")) self._remember_sizes.set_active(prefs.remember_sizes) ui.pack_start(self._remember_sizes) # set up the plot area height spinner adj = gtk.Adjustment(prefs.plot_area_height, 40, 2000, 5) spinner = gtk.SpinButton(adj, digits=0) spinner.set_size_request(60, -1) spinner.set_property('activates-default', True) def valueChanged(spinbutton, prefs): prefs.plot_area_height = spinbutton.get_value_as_int() prefs.notify() spinner.connect('value-changed', valueChanged, prefs) hbox = gtk.HBox(spacing=6) hbox.pack_start(gtk.Label(_("Plot area height:")), expand=False) hbox.pack_start(spinner, expand=False) hbox.pack_start(gtk.Label(_("pixels")), expand=False) ui.pack_start(hbox) self._plot_area_height = spinner def _setup_popup_settings(self, prefs): popups = self.newStyleFrame(_("Popup messages")) self._confirm_actions = gtk.CheckButton( _("Confirm dangerous actions")) self._confirm_actions.set_active(prefs.confirm_actions) popups.pack_start(self._confirm_actions) self._report_success = gtk.CheckButton( _("Show a popup report after successful actions")) self._report_success.set_active(prefs.report_success) popups.pack_start(self._report_success) self._show_delivery = gtk.CheckButton( _("Show verbose delivery of single messages")) self._show_delivery.set_active(prefs.show_delivery) popups.pack_start(self._show_delivery) def newStyleFrame(self, title): """Create a HIG-compliant "frame". Returns the content vbox. """ title = gtk.Label('%s' % title) title.set_property('use-markup', True) title.set_alignment(0.0, 0.5) content = gtk.VBox(spacing=6) hbox = gtk.HBox() hbox.pack_start(gtk.Label(' ' * 4), expand=False) hbox.pack_end(content) vbox = gtk.VBox() vbox.set_spacing(12) vbox.pack_start(title) vbox.pack_end(hbox) self.vbox.add(vbox) return content def apply(self, prefs): """Store settings in prefs.""" prefs.bin_dir = self._bin_dir.get_text() prefs.exim_binary = self._exim_binary.get_text() prefs.use_sudo = self._use_sudo.get_active() prefs.use_ssh = self._use_ssh.get_active() prefs.hostname = self._hostname.get_text() prefs.log_dir = self._log_dir.get_text() prefs.mainlog_name = self._mainlog_name.get_text() prefs.queue_interval = \ int(self._queue_interval.get_value() * 1000) prefs.process_interval = \ int(self._process_interval.get_value() * 1000) prefs.plotting_interval = \ int(self._plotting_interval.get_value() * 1000) prefs.confirm_actions = bool(self._confirm_actions.get_active()) prefs.report_success = bool(self._report_success.get_active()) prefs.remember_sizes = bool(self._remember_sizes.get_active()) prefs.plot_area_height = self._plot_area_height.get_value_as_int() prefs.save() prefs.notify() geximon-0.7.7/geximon/widgets.py0000644000175000017500000007307210773041334015634 0ustar daviddavid"""Widgets for Geximon""" import sys import os import datetime import signal import re # needed to escape exigrep patterns try: import gtk except RuntimeError, e: if str(e) == "could not open display": print "Could not connect to an X display" sys.exit() import gobject from exim import ProcessManager, QueueManager from gtkhelpers import Timer, WrappedTextView from gtkhelpers import PopupWindow, EntryDialog, AlertDialog __metaclass__ = type class LogWidget(WrappedTextView): """A widget that displays the tail of the exim main log.""" MAX_LOG_LINES = 10000 # maximum lines to have in the buffer at a time def __init__(self, logwatcher, prefs): WrappedTextView.__init__(self) self._track_log = prefs.track_log prefs.subscribe(self.apply_prefs) self._logwatcher = logwatcher self.buffer = self.get_buffer() self.buffer.create_tag('monospace', family='Monospace') self.buffer.create_tag('time', foreground='purple') self.buffer.create_tag('message_id', foreground='blue') self.buffer.create_tag('info', foreground='black') self.buffer.create_mark('end', self.buffer.get_end_iter(), False) self.buffer.insert_with_tags_by_name(self.buffer.get_start_iter(), _("geximon started at %s") % datetime.datetime.now(), 'monospace', 'info') self.timer = Timer(prefs.log_interval, self.update) def update(self): self._logwatcher.update() unseen = self._logwatcher.get_unseen() # remove the date (like eximon) unseen = \ map(lambda s: s[s.find(' ')+1:], unseen) # show the new data for line in unseen: # check for time signature if len(line) > 9 and line[2] == ':' and line[5] == ':': self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), "\n" + line[:9], 'monospace', 'time') else: # no time signature, print everything and continue self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), "\n" + line, 'monospace') continue # check for message id if len(line) > 25 and line[15] == '-' and line[22] == '-': self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), line[9:25], 'monospace', 'message_id') self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), line[25:], 'monospace', 'info') else: # no message id, print everything and continue self.buffer.insert_with_tags_by_name( self.buffer.get_end_iter(), line[9:], 'monospace', 'info') if unseen: # if there was new data # discard old data if there's too much of it line_count = self.buffer.get_line_count() if line_count > self.MAX_LOG_LINES: start = self.buffer.get_start_iter() middle = self.buffer.get_iter_at_line_index( int(line_count - self.MAX_LOG_LINES*0.8), 0) self.buffer.delete(start, middle) if self._track_log: self.scroll_to_mark(self.buffer.get_mark('end'), 0.0) def apply_prefs(self, prefs): self._track_log = prefs.track_log self.set_wrap_mode(prefs.wrap_log) self.timer.update_interval(prefs.log_interval) self._logwatcher.use_sudo = prefs.use_sudo self._logwatcher.use_ssh = prefs.use_ssh self._logwatcher.hostname = prefs.hostname if (self._logwatcher.log_dir != prefs.log_dir or self._logwatcher.mainlog_name != prefs.mainlog_name): self._logwatcher._valid = True self._logwatcher.open(prefs.log_dir, prefs.mainlog_name) self.update() class ProcessWidget(gtk.TreeView): """A widget that displays a list of processes.""" def __init__(self, statusbar, prefs): self._statusbar = statusbar self._old_processes = {} self.process_mgr = ProcessManager(self.do_update, prefs.bin_dir, prefs.use_sudo, prefs.use_ssh, prefs.hostname) self.model = gtk.ListStore(gobject.TYPE_INT, # 0 pid gobject.TYPE_STRING) # 1 status gtk.TreeView.__init__(self, self.model) self.connect('button-press-event', self.click) self.connect('popup-menu', self.popupMenu) renderer = gtk.CellRendererText() for index, title in enumerate(["PID", "Status"]): column = gtk.TreeViewColumn(title, renderer, text=index) column.set_reorderable(True) column.set_resizable(True) column.set_sort_column_id(index) self.append_column(column) self.get_column(0).clicked() # sort by pid self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.set_headers_clickable(True) prefs.subscribe(self.apply_prefs) self.timer = Timer(prefs.process_interval, self.update) def update(self, new_status="Running exiwhat..."): """Schedule an immediate update of process status.""" self._statusbar.pop(0) self._statusbar.push(0, new_status) self.process_mgr.schedule_update() def do_update(self, processes, info): """Update the process list. Called from a background thread. """ gtk.gdk.threads_enter() try: old_processes = self._old_processes # remove outdated entries iter = self.model.get_iter_first() while iter is not None: next = self.model.iter_next(iter) pid = self.model.get_value(iter, 0) if (pid not in processes or old_processes[pid] != processes[pid]): self.model.remove(iter) iter = next # add new and changed entries to list for pid, status in processes.iteritems(): if pid not in old_processes or old_processes[pid] != status: self.model.append((pid, status)) self._old_processes = processes self._statusbar.pop(0) self._statusbar.push(0, info) finally: gtk.gdk.threads_leave() def click(self, widget, event): """Handle a click in the process widget.""" if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: self.popupMenu(widget, event.button) return True # handled else: return False # not handled def popupMenu(self, widget, button=0): """Pop up the context menu.""" menu = ProcessContextMenu(self.get_selection(), self) menu.show_all() menu.popup(None, None, None, button, gtk.get_current_event_time()) def cleanup(self): """Clean up when the widget is destroyed.""" self.process_mgr.stop() def apply_prefs(self, prefs): self.process_mgr.bin_dir = prefs.bin_dir self.process_mgr.use_sudo = prefs.use_sudo self.process_mgr.use_ssh = prefs.use_ssh self.process_mgr.hostname = prefs.hostname self.timer.set_paused(not prefs.show_process_list) self.timer.update_interval(prefs.process_interval) class ProcessContextMenu(gtk.Menu): """Context menu for the process list.""" def __init__(self, selection, process_widget): super(ProcessContextMenu, self).__init__() self._process_widget = process_widget pids = self.selectedProcesses(selection) self.add(gtk.TearoffMenuItem()) menuitem = gtk.MenuItem(_("_Refresh")) menuitem.connect('activate', lambda menuitem: process_widget.update()) self.add(menuitem) self.add(gtk.SeparatorMenuItem()) submenu = gtk.Menu() menuitem = gtk.MenuItem(_("SIG_TERM")) menuitem.connect('activate', self._killProcesses, pids, signal.SIGTERM) submenu.add(menuitem) menuitem = gtk.MenuItem(_("SIG_KILL")) menuitem.connect('activate', self._killProcesses, pids, signal.SIGKILL) submenu.add(menuitem) menuitem = gtk.MenuItem(_("_Signal")) menuitem.set_submenu(submenu) self.add(menuitem) def selectedProcesses(self, selection): """Return selected processes.""" pids = [] def callback(model, path, iter): pid = model.get_value(iter, 0) pids.append(pid) selection.selected_foreach(callback) return pids def _killProcesses(self, menuitem, pids, signal): """Send a signal to selected processes.""" killed = [] permissions_ok = True for pid in pids: try: os.kill(pid, signal) killed.append(pid) except OSError, e: if e.errno == 1: # permission denied permissions_ok = False if not permissions_ok: status = _("Permission problems!") elif not killed: status = _("Process has already terminated.") else: process_word = len(killed) > 1 and _("processes") or _("process") status = _("Signal %d sent to %s %s.") % \ (signal, process_word, suffix, ", ".join(map(str, killed))) self._process_widget.update(new_status=status) class QueueWidget(gtk.TreeView): """A widget that displays the exim message queue.""" def __init__(self, main_win, logwatcher, queue_mgr, prefs): self._main_win = main_win # needed for popups self._statusbar = main_win.statusbar self._old_queue = {} self.queue_mgr = queue_mgr self.queue_mgr.callback = self.do_update self.logwatcher = logwatcher self.model = gtk.ListStore(gobject.TYPE_STRING, # 0 color gobject.TYPE_STRING, # 1 message id gobject.TYPE_STRING, # 2 sender gobject.TYPE_STRING, # 3 size gobject.TYPE_STRING, # 4 time in queue gobject.TYPE_STRING) # 5 recipients # GTK's recent addition, 'fixed_height_mode' would be really useful # to speed things up, however, it is not yet supported by pyGTK renderer = gtk.CellRendererText() renderer.set_property('family', 'Monospace') id_column = gtk.TreeViewColumn(_("Message ID"), renderer, text=1) id_column.add_attribute(renderer, 'foreground', 0) gtk.TreeView.__init__(self, self.model) self.connect('button-press-event', self.click) self.connect('popup-menu', self.popupMenu) self.total_str = "" self.selected_str = "" self.get_selection().connect('changed', self.selectionChanged) renderer = gtk.CellRendererText() columns = ([id_column] + [gtk.TreeViewColumn(title, renderer, text=source) for source, title in [(2, _("Sender")), (3, _("Size")), (4, _("Time")), (5, _("Recipients"))]]) for index, column in enumerate(columns): column.set_reorderable(True) column.set_resizable(True) column.set_sort_column_id(index + 1) # not very neat, but it works self.append_column(column) self._setUpSorting() self.get_selection().set_mode(gtk.SELECTION_MULTIPLE) self.set_headers_clickable(True) self.set_rules_hint(True) self._initializing = True self.confirm_actions = prefs.confirm_actions self.report_success = prefs.report_success prefs.subscribe(self.apply_prefs) self.timer = Timer(prefs.queue_interval, self.update) def _setUpSorting(self): """Set up correct sorting by size and time.""" def sort_func(treemodel, iter1, iter2, data): col_number, coef = data val1 = treemodel.get_value(iter1, col_number) val2 = treemodel.get_value(iter2, col_number) def eval_suffix(s, coef): if s is None or len(s) == 0: return 0 suffix = s[-1] if suffix in coef: return float(s[:-1]) * coef[suffix] else: return float(s) result = cmp(eval_suffix(val1, coef), eval_suffix(val2, coef)) return result self.model.set_sort_func(3, sort_func, (3, {'K': 1024, 'M': 1048576})) self.model.set_sort_func(4, sort_func, (4, {'m': 1, 'h': 60, 'd': 24*60})) def update(self): """Schedule an immediate update of the queue list.""" self.queue_mgr.schedule_update() def do_update(self, queue): """Update the process list. Called from a background thread. """ # XXX this method is too long and needs to be split up gtk.gdk.threads_enter() if self._initializing: # the first update tends to be massive, so it is worth unbinding # the model from the view temporarily for performance reasons self.set_model(None) self.model.set_sort_column_id(0, gtk.SORT_ASCENDING) old_queue = self._old_queue # remove messages no longer in the queue from display iter = self.model.get_iter_first() while iter is not None: next = self.model.iter_next(iter) id = self.model.get_value(iter, 1) if id not in queue: self.model.remove(iter) elif queue[id] != old_queue[id]: msg = queue[id] self.model.set(iter, 0, msg.frozen and "#FF0000" or "#000000", 1, msg.id, 2, msg.sender, 3, msg.size, 4, msg.time, 5, " ".join(msg.recipients)) iter = next gtk.gdk.threads_leave() # it is safe to do this now because the obsolete messages have been # removed and only new ones will be added self._old_queue = queue # find all messages which should be added to the model new_rows = [] for id in queue: if id not in old_queue: msg = queue[id] row = (msg.frozen and "#FF0000" or "#000000", msg.id, msg.sender, msg.size, msg.time, " ".join(msg.recipients)) new_rows.append(row) # reflect that the list is being updated in the statusbar; # only bother if there are many new messages worth_bothering = len(queue) > 100 and len(new_rows) > 10 if self._initializing or worth_bothering: self.total_str = (_("Updating message list...")) gtk.gdk.threads_enter() self.updateStatusbar() gtk.gdk.threads_leave() if self._initializing: # the model is unbound so there is no need to call threads_enter() # and threads_leave() in every iteration; once is enough gtk.gdk.threads_enter() for row in new_rows: self.model.append(row) gtk.gdk.threads_leave() else: # the model is bound, so we need to do things the slow way # temporarily disabling sorting helps quite a bit if worth_bothering: gtk.gdk.threads_enter() # I HATE THESE! sort_mode = self.model.get_sort_column_id() self.model.set_sort_column_id(0, gtk.SORT_ASCENDING) gtk.gdk.threads_leave() for row in new_rows: gtk.gdk.threads_enter() self.model.append(row) gtk.gdk.threads_leave() if worth_bothering: gtk.gdk.threads_enter() self.model.set_sort_column_id(*sort_mode) gtk.gdk.threads_leave() # update the statusbar data msg_word = len(queue) > 1 and _("messages") or _("message") frozen = len(filter(lambda id: queue[id].frozen, queue)) self.total_str = (_("%d %s in queue (%d frozen).") % (len(queue), msg_word, frozen)) gtk.gdk.threads_enter() self.updateStatusbar() if self._initializing: self.set_model(self.model) # rebind the model self.model.set_sort_column_id(1, gtk.SORT_ASCENDING) self._initializing = False gtk.gdk.threads_leave() def click(self, widget, event): """Handle a click in the queue widget.""" if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: self.popupMenu(widget, event.button) return True # handled else: return False # not handled def popupMenu(self, widget, button=0): """Pop up the context menu.""" menu = QueueContextMenu(self.get_selection(), self._main_win, self) menu.show_all() menu.popup(None, None, None, button, gtk.get_current_event_time()) def selectionChanged(self, selection): """Update statusbar on selection change.""" messages = [0] def callback(model, path, iter): messages[0] += 1 selection.selected_foreach(callback) messages = messages[0] # this could be done with a single line: # messages = selection.count_selected_rows() # but I think the current way is more compatible # with older versions of pygtk if messages: msg_word = messages > 1 and _("messages") or _("message") self.selected_str = _("%d %s selected.") % (messages, msg_word) else: self.selected_str = "" self.updateStatusbar() def updateStatusbar(self): """Update the statusbar.""" # XXX this gets called from multiple threads, locking would be nice self._statusbar.pop(0) self._statusbar.push(0, self.total_str + ' ' + self.selected_str) def cleanup(self): """Clean up when the widget is destroyed.""" self.queue_mgr.stop() def apply_prefs(self, prefs): self.queue_mgr.bin_dir = prefs.bin_dir self.queue_mgr.use_sudo = prefs.use_sudo self.queue_mgr.use_ssh = prefs.use_ssh self.queue_mgr.hostname = prefs.hostname self.confirm_actions = prefs.confirm_actions self.report_success = prefs.report_success self.timer.update_interval(prefs.queue_interval) self.update() class QueueContextMenu(gtk.Menu): """Context menu for queued messages.""" removeInfo = (_("removed"), _("The message(s) will be irreversibly deleted and " "no bounce messages will be sent.")) giveupInfo = (_("discarded"), _("The message(s) will be deleted and a delivery error message " "will be sent to the sender.")) markInfo = (_("marked as delivered"), _("The recipients who have not yet been processed will not " "receive the message(s).")) def __init__(self, selection, main_win, queue_widget): # XXX should be split up into several methods gtk.Menu.__init__(self) self._queue_widget = queue_widget queue_mgr = queue_widget.queue_mgr self.logwatcher = queue_widget.logwatcher self._main_win = main_win self.confirm_actions = queue_widget.confirm_actions self.report_success = queue_widget.report_success queue = queue_widget._old_queue message_ids = self.selectedMessageIds(selection) messages = [] for msg_id in message_ids[:]: if msg_id in queue: messages.append(queue[msg_id]) else: # the queue view is inconsistent with internal data message_ids.remove(msg_id) # XXX ignore the message all_frozen = True all_unfrozen = True for msg in messages: if not msg.frozen: all_frozen = False else: all_unfrozen = False sel_none = len(message_ids) == 0 sel_single = len(message_ids) == 1 first_msg = sel_single and message_ids[0] or None def add_item(menu, label, sensitive, callback, *args): menuitem = gtk.MenuItem(label) menuitem.set_sensitive(sensitive) menuitem.connect('activate', callback, *args) menu.add(menuitem) self.add(gtk.TearoffMenuItem()) add_item(self, _("_Refresh list"), True, lambda menuitem: queue_widget.update()) self.add(gtk.SeparatorMenuItem()) show_menu = gtk.Menu() add_item(show_menu, _("_Body"), sel_single, self._exim_popup, queue_mgr.getMessageBody, first_msg, _("Body of")) add_item(show_menu, _("_Headers"), sel_single, self._exim_popup, queue_mgr.getMessageHeaders, first_msg, _("Headers of")) add_item(show_menu, _("_Compete Message"), sel_single, self._exim_popup, queue_mgr.getMessageAll, first_msg, _("Display of")) add_item(show_menu, _("_Message log"), sel_single, self._exim_popup, queue_mgr.getMessageLog, first_msg, _("Log of")) show_item = gtk.MenuItem(_("_Show")) show_item.set_submenu(show_menu) self.add(show_item) self.add(gtk.SeparatorMenuItem()) grep_menu = gtk.Menu() params = (not sel_none, self._exigrep, message_ids) add_item(grep_menu, _("_ID"), not sel_none, self._exigrep, messages, 'id') add_item(grep_menu, _("_Sender"), not sel_none, self._exigrep, messages, 'sender') add_item(grep_menu, _("_Recipients"), not sel_none, self._exigrep, messages, 'recipients') grep_item = gtk.MenuItem(_("_Exigrep")) grep_item.set_submenu(grep_menu) self.add(grep_item) self.add(gtk.SeparatorMenuItem()) add_item(self, _("_Freeze"), not all_frozen, self._exim_action, None, queue_mgr.freezeMessages, message_ids) add_item(self, _("_Thaw"), not all_unfrozen, self._exim_action, None, queue_mgr.thawMessages, message_ids) self.add(gtk.SeparatorMenuItem()) add_item(self, _("Re_move"), not sel_none, self._exim_action, self.removeInfo, queue_mgr.removeMessages, message_ids) add_item(self, _("_Attempt to deliver"), not sel_none, self._exim_action, None, queue_mgr.deliverMessages, message_ids) add_item(self, _("_Give up"), not sel_none, self._exim_action, self.giveupInfo, queue_mgr.giveUpMessages, message_ids) add_item(self, _("Mark as _delivered"), not sel_none, self._exim_action, self.markInfo, queue_mgr.markAllDelivered, message_ids) self.add(gtk.SeparatorMenuItem()) add_item(self, _("_Change sender"), sel_single, self._exim_entry, queue_mgr.editSender, first_msg, _("Change the sender of "), _("Sender address:")) add_item(self, _("Add reci_pients"), sel_single, self._exim_entry, queue_mgr.addRecipients, first_msg, _("Add recipients to "), _("Enter the new recipients:")) def selectedMessageIds(self, selection): """Return ids of selected messages.""" messages = [] def callback(model, path, iter): messages.append(model[path[0]][1]) selection.selected_foreach(callback) return messages def _exim_action(self, menuitem, msg_info, callable, message_ids): """Perform an action on message(s). Shows the user a dialog requesting to confirm the action. Calls `callable`, shows a dialog with the result. `callable` is expected to return a tuple: (action_successful, message). `msg_info` is a tuple of two strings describing the action. `message_ids` is a list of strings (message ids). """ if self.confirm_actions and msg_info: num = len(message_ids) > 1 and (" " + str(len(message_ids))) or "" msg_word = len(message_ids) > 1 and _("messages") or _("message") dialog = AlertDialog(self._main_win, _("The%s selected %s will be %s.") % (num, msg_word, msg_info[0]), msg_info[1], query=True) response = dialog.ask() if not response: return (success, status_msg) = callable(message_ids) if status_msg and (not success or self.report_success): if success: AlertDialog(self._main_win, _("Action successful."), status_msg).ask() else: AlertDialog(self._main_win, _("There were problems."), status_msg, error=True).ask() self._queue_widget.update() def _exim_popup(self, menuitem, callable, id, title_prefix): """Show a popup window with the string returned by callable.""" (success, text) = callable(id) if success: popup = PopupWindow(title_prefix + id, text) popup.show_all() else: AlertDialog(self._main_win, _("An error occured."), text, error=True).ask() def _exim_entry(self, menuitem, callable, id, title_prefix, label_text): """Show an entry dialog, input data ant pass it to `callable`. `id` is a string (the message id). `title_prefix` is the prefix for the entry window title. `label_text` is the text of the label in the dialog.""" popup = EntryDialog(main_window=self._main_win, title=(title_prefix + " " + id), label_text=label_text) input = popup.get_input() if input: (success, status_msg) = callable(id, input) if not success: AlertDialog(self._main_win, _("An error occured."), status_msg, error=True).ask() elif self.report_success: AlertDialog(self._main_win, _("Action successful."), status_msg).ask() self._queue_widget.update() def _exigrep(self, menuitem, messages, field): """Run exigrep with data of selected messages. `messages` is a list of Message objects (not ids). `field` is a string indicating the field to run exigrep on. """ if field == 'id': strings = [msg.id for msg in messages] elif field == 'sender': strings = [msg.sender for msg in messages] elif field == 'recipients': strings = [] for msg in messages: strings += msg.recipients else: raise ValueError("Invalid field identifier.") pattern = '|'.join([re.escape(s) for s in strings]) text = self.logwatcher.runExigrep(pattern, False, True) if text: PopupWindow("Exigrep: " + pattern, text.strip()).show_all() else: text = _() AlertDialog(self._main_win, "Empty output from exigrep.", _("exigrep returned no data.")).ask() class ExigrepDialog(EntryDialog): """An entry dialog that asks for information to pass to exigrep, invokes exigrep and shows the output in a popup window.""" def __init__(self, main_window=None, text=""): EntryDialog.__init__(self, main_window, title=_("Run exigrep"), label_text=_("Search for:")) self._main_win = main_window self._statusbar = main_window.statusbar self.entry.set_text(text) self.regexp = gtk.CheckButton(_("Parse as a regular expression")) self.regexp.set_active(True) self.all_logs = gtk.CheckButton(_("Scan all logfiles")) self.all_logs.set_active(True) checkbuttons = gtk.VBox() checkbuttons.add(self.regexp) checkbuttons.add(self.all_logs) self.vbox.add(checkbuttons) def go(self, logwatcher): pattern = self.get_input() if pattern: text = logwatcher.runExigrep(pattern, not self.regexp.get_active(), self.all_logs.get_active()) if text: popup = PopupWindow("Exigrep: " + pattern, text.strip()) popup.show_all() else: text = _("Empty output from exigrep.") AlertDialog(self._main_win, text, _("exigrep returned no data. You may want to " "refine your search terms or try a search on all " "the log files.")).ask() class EximstatsDialog(EntryDialog): """An entry dialog that asks for parameters and options to eximstats, invokes them and shows output in a popup window.""" def __init__(self, main_window=None): EntryDialog.__init__(self, main_window, title=_("Run eximstats"), label_text=_("Arguments to eximstats:")) self._statusbar = main_window.statusbar self.all_logs = gtk.CheckButton(_("Scan all logfiles")) self.all_logs.set_active(True) self.vbox.add(self.all_logs) def go(self, logwatcher): self.show_all() response = self.run() args = self.entry.get_text() all_logs = self.all_logs.get_active() self.destroy() if response == gtk.RESPONSE_OK: text = logwatcher.runEximstats(args, all_logs) popup = PopupWindow("Eximstats", text.strip()) popup.show_all() geximon-0.7.7/pixmaps/0000755000175000017500000000000010470723547013625 5ustar daviddavidgeximon-0.7.7/pixmaps/geximon-16x16.xpm0000644000175000017500000000665510470723547016620 0ustar daviddavid/* XPM */ static char * geximon_16x16_xpm[] = { "16 16 179 2", " c None", ". c #B1BAC3", "+ c #6D747E", "@ c #3B414C", "# c #292E39", "$ c #00010D", "% c #000005", "& c #0E1420", "* c #0C1421", "= c #070F1B", "- c #07101D", "; c #000002", "> c #00000C", ", c #323844", "' c #393F49", ") c #6A717B", "! c #A6AEB8", "~ c #4D535D", "{ c #646C76", "] c #ABB0B7", "^ c #D1D3D6", "/ c #F2F6F9", "( c #FFFFFF", "_ c #FEFFFF", ": c #FCFFFF", "< c #F9FDFF", "[ c #F4F9FD", "} c #CED6DC", "| c #B9C1C8", "1 c #7D8790", "2 c #24303B", "3 c #40454F", "4 c #212530", "5 c #91A1AB", "6 c #BEC9D1", "7 c #F7F9F5", "8 c #F3F5F7", "9 c #EDF0EF", "0 c #E6E8EB", "a c #E5EAEE", "b c #E2E7EC", "c c #DFE4E9", "d c #DBE2E7", "e c #D8DFE5", "f c #DBE2E9", "g c #5C6F7B", "h c #59707E", "i c #1E212C", "j c #161A25", "k c #D1D9DF", "l c #758995", "m c #AEB9C1", "n c #EAEEF1", "o c #E7EBEF", "p c #E4E8EC", "q c #DBE1E6", "r c #A1A6AB", "s c #6E7177", "t c #AEB5BB", "u c #D3DBE2", "v c #71828D", "w c #496572", "x c #778D9C", "y c #10141F", "z c #151924", "A c #EEF3F6", "B c #8A9CA7", "C c #688490", "D c #B2BDC4", "E c #DEE4E9", "F c #DBE1E7", "G c #CAD1D8", "H c #2C2F30", "I c #8B8A77", "J c #656461", "K c #444C52", "L c #354F60", "M c #748A97", "N c #8B9EAC", "O c #0B0F1A", "P c #EDF2F6", "Q c #CBD4DC", "R c #677E8C", "S c #465A65", "T c #BAC3CD", "U c #E3E8EF", "V c #D8DEE7", "W c #94959D", "X c #2A2A2B", "Y c #AFA6A3", "Z c #6E6D64", "` c #29363C", " . c #778C99", ".. c #A9B9CA", "+. c #0A0D18", "@. c #ECF1F5", "#. c #D1D9E0", "$. c #A4B3BE", "%. c #031B26", "&. c #32525D", "*. c #25353F", "=. c #4E5F6A", "-. c #476D78", ";. c #0D2029", ">. c #484847", ",. c #9A988E", "'. c #000000", "). c #ABBAC3", "!. c #ACBAC7", "~. c #040712", "{. c #6B818C", "]. c #8A98A1", "^. c #52636C", "/. c #4C4D4C", "(. c #2E2626", "_. c #294149", ":. c #0A0F12", "<. c #BBB9AF", "[. c #675248", "}. c #D0D9DF", "|. c #B3C3D0", "1. c #030510", "2. c #EBF0F4", "3. c #96A3AC", "4. c #687984", "5. c #E1E5EB", "6. c #AFB1B6", "7. c #3C3E3C", "8. c #B5B1AB", "9. c #5A5957", "0. c #DCD5CC", "a. c #C8C2B7", "b. c #79756A", "c. c #2B2A24", "d. c #050E16", "e. c #8C97A3", "f. c #030813", "g. c #BAC2C9", "h. c #556975", "i. c #C2C6D0", "j. c #C0CAD4", "k. c #B5BFC8", "l. c #787E85", "m. c #434243", "n. c #AFAFAA", "o. c #79655B", "p. c #757167", "q. c #CEC8BA", "r. c #B3AEA4", "s. c #4F4E49", "t. c #424852", "u. c #35414D", "v. c #73828F", "w. c #A2B1BF", "x. c #A6B5C3", "y. c #A4B3C0", "z. c #99A9B7", "A. c #6D7B85", "B. c #3B4258", "C. c #4E555C", "D. c #717B87", "E. c #353C4D", "F. c #44423B", "G. c #CCC7B8", "H. c #C1BDB1", "I. c #9E998A", "J. c #3A404A", "K. c #010A16", "L. c #0B1420", "M. c #0F1823", "N. c #0A131D", "O. c #09101B", "P. c #08111B", "Q. c #060913", "R. c #0A0D14", "S. c #0B0906", "T. c #9A978D", "U. c #D8D2C5", "V. c #48463F", " ", " ", ". + @ # $ % & * = - ; > , ' ) ! ", "~ { ] ^ / ( ( _ : < [ } | 1 2 3 ", "4 5 6 7 8 9 0 a b c d e f g h i ", "j k l m n o p q r s t u v w x y ", "z A B C D E F G H I J K L M N O ", "z P Q R S T U V W X Y Z ` ...+.", "z @.#.$.%.&.*.=.-.;.>.,.'.).!.~.", "z @.#.{.].^./.(._.:.<.[.'.}.|.1.", "z 2.3.4.5.6.7.8.9.0.a.b.c.d.e.f.", "4 g.h.i.j.k.l.m.n.o.'.p.q.r.s.'.", "t.u.v.w.x.y.z.A.B.C.D.E.F.G.H.I.", "! ~ J.$ $ K.L.M.N.O.P.Q.R.S.T.U.", " '.V.", " "}; geximon-0.7.7/pixmaps/geximon-32x32.xpm0000644000175000017500000002147510470723547016611 0ustar daviddavid/* XPM */ static char * geximon_32x32_xpm[] = { "32 32 424 2", " c None", ". c #040405", "+ c #000000", "@ c #3E3E3E", "# c #838383", "$ c #6B6B6B", "% c #0A0A0A", "& c #030302", "* c #090708", "= c #737473", "- c #777486", "; c #ACB9AC", "> c #FFFFFF", ", c #EEEDED", "' c #191819", ") c #8B8B82", "! c #EBEAED", "~ c #F1F3E8", "{ c #F6F2FF", "] c #EAE6FA", "^ c #F0EFF4", "/ c #CBE8D2", "( c #D3D0E2", "_ c #473F59", ": c #7A7B7A", "< c #F6F6FF", "[ c #FBF9FB", "} c #E3DDFB", "| c #E6E1FB", "1 c #C9C3FB", "2 c #C8C4DB", "3 c #C6C2F5", "4 c #BDC2C6", "5 c #C5C1F5", "6 c #E2BEF5", "7 c #E4E4EC", "8 c #626169", "9 c #656565", "0 c #090909", "a c #474747", "b c #F0EEFC", "c c #DDD8FB", "d c #D8D2FB", "e c #D4CEFB", "f c #CECAE1", "g c #C4BDF8", "h c #C6C2D9", "i c #E8E6D5", "j c #E8E6F3", "k c #ECEBF4", "l c #FCFBF6", "m c #F9FFFF", "n c #D6DAB1", "o c #9C9C9C", "p c #54564E", "q c #8B8985", "r c #F7F8F7", "s c #FFFBFC", "t c #A2A3A2", "u c #303030", "v c #020201", "w c #E3E1EA", "x c #F0EEFB", "y c #ECEBDF", "z c #ECEBD9", "A c #ECEBD5", "B c #F4F4F4", "C c #F5F5F7", "D c #F0F3FA", "E c #E1DFE8", "F c #9996A1", "G c #474253", "H c #4F4F50", "I c #A8A8A8", "J c #D6DEF7", "K c #8D8D8D", "L c #EBEBEB", "M c #070706", "N c #F1F1EF", "O c #F6F5FC", "P c #D5D8E0", "Q c #E8E1FF", "R c #F6EEFF", "S c #D2D4FF", "T c #9086A7", "U c #3A3845", "V c #4C484E", "W c #9997A1", "X c #F7F7EB", "Y c #97A0C2", "Z c #F0E8F3", "` c #E3E3E3", " . c #737373", ".. c #D4D4D4", "+. c #121212", "@. c #BDBDBD", "#. c #FFFBFF", "$. c #DDD6F8", "%. c #ECE4FF", "&. c #64646F", "*. c #211F24", "=. c #77757D", "-. c #C0C194", ";. c #FFFFF9", ">. c #C7C9EF", ",. c #D2D4EB", "'. c #8992B8", "). c #0D15A3", "!. c #8F908A", "~. c #B6B6B6", "{. c #555555", "]. c #636264", "^. c #D4D8F8", "/. c #868393", "(. c #6F6D74", "_. c #030300", ":. c #CCC8C4", "<. c #7A7BBA", "[. c #B8BAE3", "}. c #6A6DB4", "|. c #6B6FB0", "1. c #161AAA", "2. c #7679B4", "3. c #757CB8", "4. c #DAD8E2", "5. c #77796D", "6. c #E5E4E7", "7. c #B4B4B4", "8. c #0E0E0E", "9. c #010101", "0. c #060607", "a. c #52524E", "b. c #969693", "c. c #CBCCA5", "d. c #999CB4", "e. c #6A6962", "f. c #5C60CA", "g. c #454BAC", "h. c #6E72B3", "i. c #8286B9", "j. c #BEC2E4", "k. c #FFFFFB", "l. c #ACACAC", "m. c #DBDBDB", "n. c #E2E2E2", "o. c #181818", "p. c #5B5B5B", "q. c #939393", "r. c #D8D8D6", "s. c #E5D5F7", "t. c #151DC2", "u. c #9B9CAB", "v. c #665E4E", "w. c #636253", "x. c #5A5D72", "y. c #CFCDDA", "z. c #BDC1E3", "A. c #DEE0EA", "B. c #DAD6D6", "C. c #858589", "D. c #EEEEED", "E. c #BFBFBF", "F. c #272727", "G. c #A5A5A5", "H. c #EAEAEA", "I. c #9F9F9F", "J. c #FFFFF2", "K. c #A2A5D3", "L. c #9DA4DC", "M. c #4C4FB3", "N. c #6D6B5B", "O. c #2B3144", "P. c #B8BCE2", "Q. c #EFF0EF", "R. c #F2F5EF", "S. c #8D8D8B", "T. c #B1B1B1", "U. c #BEBEBE", "V. c #BABABA", "W. c #D3D3D3", "X. c #CECECE", "Y. c #353535", "Z. c #141414", "`. c #BABAB5", " + c #9A9A9A", ".+ c #2D2D2B", "++ c #7E7D7B", "@+ c #25282A", "#+ c #5258D0", "$+ c #D0D5FF", "%+ c #271D18", "&+ c #AEAC85", "*+ c #FFFAF2", "=+ c #5A5954", "-+ c #54565D", ";+ c #EDEEED", ">+ c #ECECEC", ",+ c #868687", "'+ c #CBCBCB", ")+ c #7C7C7C", "!+ c #040404", "~+ c #FBFAFF", "{+ c #BCB8AA", "]+ c #BEBCBA", "^+ c #B0ACA5", "/+ c #181915", "(+ c #AAA4A5", "_+ c #E7EFF0", ":+ c #989A9A", "<+ c #E0E0DF", "[+ c #B3B3B3", "}+ c #B8B8B8", "|+ c #2F2F2F", "1+ c #020202", "2+ c #2E2E2E", "3+ c #F2F2F2", "4+ c #C9C9C9", "5+ c #030205", "6+ c #C2BEB6", "7+ c #A8A59F", "8+ c #ECE8DE", "9+ c #ACA8A0", "0+ c #100902", "a+ c #070000", "b+ c #6E3234", "c+ c #8BA8A8", "d+ c #F1F4F4", "e+ c #D2D7D6", "f+ c #AFAFAF", "g+ c #C4C4C4", "h+ c #595959", "i+ c #0B0B0B", "j+ c #111011", "k+ c #C6C7C6", "l+ c #F5F5F5", "m+ c #E6E6E6", "n+ c #919191", "o+ c #E6E6DF", "p+ c #A4A079", "q+ c #5C5A54", "r+ c #EAE7DD", "s+ c #EEE8DF", "t+ c #F5F3E9", "u+ c #C9D9D3", "v+ c #110200", "w+ c #E5EBF0", "x+ c #EDDFDF", "y+ c #D2D2D2", "z+ c #A3A3A3", "A+ c #AEAEAE", "B+ c #BCBCBC", "C+ c #9B9B9B", "D+ c #222222", "E+ c #8B8B8B", "F+ c #F1F1F1", "G+ c #E2E2DF", "H+ c #9B9BB8", "I+ c #020100", "J+ c #1C1A23", "K+ c #6F6F8C", "L+ c #262A2B", "M+ c #040000", "N+ c #A5A39B", "O+ c #CFCAC1", "P+ c #EEF7E4", "Q+ c #C2C4B9", "R+ c #000100", "S+ c #CACFD1", "T+ c #E5E4E6", "U+ c #CACACA", "V+ c #4D4D4D", "W+ c #060606", "X+ c #4B4B4B", "Y+ c #FAFAFA", "Z+ c #F9F9F9", "`+ c #F0F0F0", " @ c #D3D3D1", ".@ c #CACAC7", "+@ c #CACCC4", "@@ c #E0E7E7", "#@ c #695455", "$@ c #5D5751", "%@ c #DDD9D1", "&@ c #CDC8BF", "*@ c #E2DED2", "=@ c #DAD6C8", "-@ c #0D0E0F", ";@ c #A6A6A6", ">@ c #C6C6C7", ",@ c #BBBBBB", "'@ c #232223", ")@ c #1A1A1A", "!@ c #D7D7D7", "~@ c #FCFCFC", "{@ c #F3F3F3", "]@ c #DFDFE0", "^@ c #DEDEDE", "/@ c #DBDCDB", "(@ c #DADADA", "_@ c #C5D6BF", ":@ c #620014", "<@ c #BFC0B6", "[@ c #D5D2C8", "}@ c #CFCBC1", "|@ c #B0ACA7", "1@ c #282521", "2@ c #424141", "3@ c #AEAFAE", "4@ c #B0B0B0", "5@ c #A0A0A0", "6@ c #3B3B3B", "7@ c #0C0C0C", "8@ c #050505", "9@ c #E9EAE9", "0@ c #E7E7E7", "a@ c #DFDFDF", "b@ c #DDDDDD", "c@ c #D7F4F0", "d@ c #F7FFFF", "e@ c #B5AAA8", "f@ c #874849", "g@ c #B5B3A7", "h@ c #CBC79B", "i@ c #FCFCF5", "j@ c #7F7D77", "k@ c #1C1712", "l@ c #4C4C4A", "m@ c #8A8A8A", "n@ c #6A6A6A", "o@ c #222122", "p@ c #5C5C5C", "q@ c #DFE0DF", "r@ c #9C8B8B", "s@ c #897676", "t@ c #5F3A3B", "u@ c #9F8E8E", "v@ c #CCEDED", "w@ c #4C4A49", "x@ c #645D55", "y@ c #E4E1DA", "z@ c #C6B4A9", "A@ c #CDC9C0", "B@ c #E8E5DC", "C@ c #89867F", "D@ c #262521", "E@ c #2C2C2C", "F@ c #E2DBDB", "G@ c #D1C6C6", "H@ c #A89797", "I@ c #977D7E", "J@ c #885756", "K@ c #897070", "L@ c #6E3C40", "M@ c #784548", "N@ c #AFB0B1", "O@ c #3B3F40", "P@ c #14100D", "Q@ c #ECEAE4", "R@ c #DCD8D0", "S@ c #B9B5AA", "T@ c #DCD8CF", "U@ c #D5D2C9", "V@ c #85827C", "W@ c #0F0F0F", "X@ c #F6F6F6", "Y@ c #C5BABA", "Z@ c #AFA1A1", "`@ c #AA8A8B", " # c #9F8586", ".# c #987E7F", "+# c #9DB0B0", "@# c #C5B8B9", "## c #CAD4D0", "$# c #FCFFFF", "%# c #F9FDFD", "&# c #D9D8D8", "*# c #A5A4A4", "=# c #08080A", "-# c #4F4C48", ";# c #F6F1EC", "># c #F8F6F0", ",# c #CAC7BF", "'# c #211F19", ")# c #A7A49B", "!# c #4A4946", "~# c #6B6C6B", "{# c #EBE4E4", "]# c #B09798", "^# c #A3898A", "/# c #B29499", "(# c #977979", "_# c #BB8D8E", ":# c #E9E9E9", "<# c #727372", "[# c #0E0F0E", "}# c #12100B", "|# c #8B8883", "1# c #D6D1CB", "2# c #82807C", "3# c #E2E0DC", "4# c #52504D", "5# c #3F3F3F", "6# c #DBDFDF", "7# c #B2999A", "8# c #AF9697", "9# c #A98F90", "0# c #A08787", "a# c #E0E4E4", "b# c #CDCDCD", "c# c #030303", "d# c #524F4C", "e# c #95918A", "f# c #747167", "g# c #0F0E0C", "h# c #1F1E1F", "i# c #CDC4C4", "j# c #D4CBCB", "k# c #4F4F4F", "l# c #0D0D0D", "m# c #D1D1D1", "n# c #333333", "o# c #424242", "p# c #151515", "q# c #080808", " ", " ", " ", " . + @ # $ + ", " % & * = - ; > , > > > + ", " + ' = # ) ! ~ { > > ] ^ ^ / ( > _ + + + ", " : < > > [ } | | 1 2 3 4 5 6 7 > 8 + = 9 0 ", " a b c d e f g h i j k l m n o p q r s t u + ", " v w ] x y z A B C D E F G H I J > > K L # + ", " M N O P k Q R S T U V W X > > Y Z ` .> ..+.+ ", " @.#.$.%.&.*.=.-.> ;.>.> ,.'.).> !.~.` B {.+ ", " ].^./.(._.:.+ <.> [.}.|.1.2.3.4.5.6.` ` 7.8. ", " 9.+ 0.a.b.c.d.+ > > e.+ f.g.h.i.j.k.o l.` ` m.n.a + ", " o.p.q.r.> > s.t.u.+ v.> w.x.y.z.,.A.B.C.D.@.E.m.m.q.+ ", " F.G.H.> > I.J.K.L.M.+ > N.O.P.L Q.R.S.T.` U.m.V.W.X.Y.9. ", " Z.q.`. +.+++@+#+$+%+&+*+=+-+> ;+>+W.,+m.m.m.7.W.W.'+)++ ", " !+)+> ~++ > {++ + ]+> ^+/++ (+_+> :+'+<+m.[+W.W.7.'+}+|+1+ ", " + 2+3+> 4+5+> > > > 6+7+8+9+0+a+b+c+d+e+B.W.l.f+'+g+..h+i+ ", " j+k+l+m+n++ o+> p++ q+r+s+t+u+v++ w+x+y+z+'+'+A+g+B+C+D+ ", " !+E+> F+G+H+I++ J+K+L+M+N+> O+P+Q+R++ S+T+U+g+g+I B+E.V+0 ", " W+X+Y+Z+`+` @y+.@+@@@#@+ $@> %@&@*@=@+ -@;@>@,@B+B+7.# '@ ", " )@!@~@Z+{@]@^@/@(@!@_@:@+ + <@> [@}@> |@1@2@3@4@[+B+5@6@7@", " 8@C+> ~@3+`+9@0@a@b@c@d@e@f@+ + > > g@h@i@j@k@l@m@n@@ o@+ ", " 8@p@> B q@Q.L L ` y+r@s@t@u@v@w@+ x@> y@z@A@B@C@D@+ W++ ", " E@b@B B >+F@G@H@I@J@K@L@M@V.A+N@O@P@Q@> R@S@T@U@V@ ", " W@l.X@Y@Z@`@ #I@.#+#@###$#%#&#*#{.=#+ -#;#>#,#'#)#!# ", " 1+~#{#]#^#/#(#_#'+W.`+:#C+<#[#+ + + }#|#1#2#3#4# ", " + 5#6#7#8#9#0#@#a#b## 2++ + + + c#d#e#f#g# ", " h#f+i#@#j#@.E+k#l#+ + ", " +.)+m#C+$ Y.i+1++ ", " % n#o#p#0 q# ", " % + "}; geximon-0.7.7/scripts/0000755000175000017500000000000010470723547013633 5ustar daviddavidgeximon-0.7.7/scripts/geximon0000755000175000017500000000014510470723547015227 0ustar daviddavid#!/usr/bin/python """A wrapper script to run geximon.""" from geximon import geximon geximon.main() geximon-0.7.7/setup.py0000755000175000017500000000354311036202732013651 0ustar daviddavid#!/usr/bin/python import distutils.core VERSION = '0.7.7' long_description = \ """Geximon is a monitor for the exim mail server. It has all the features of the original program, eximon, plus some more, and looks nicer.""" distutils.core.setup( name='geximon', version=VERSION, description="exim MTA monitor", long_description=long_description, url='http://geximon.planetwatson.co.uk/', scripts=['scripts/geximon'], data_files=[('share/doc/geximon', ['README', 'doc/geximon.html']), ('share/doc/geximon/docbook', ['doc/geximon-C.xml']), ('share/man/man8', ['doc/geximon.8']), ('share/omf/geximon', ['doc/geximon-C.omf']), ('share/pixmaps', ['pixmaps/geximon-16x16.xpm', 'pixmaps/geximon-32x32.xpm']), # XXX lithuanian translation ('share/locale/lt/LC_MESSAGES', ['translation/geximon.mo'])], packages = ['geximon'], author="Gintautas Miliauskas", author_email="gintas@pov.lt", maintainer="David Watson", maintainer_email="david@planetwatson.co.uk", license='GPL', platforms='POSIX', classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: X11 Applications :: Gnome', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Operating System :: POSIX', 'Programming Language :: Python', 'Topic :: System :: Networking :: Monitoring', ] ) # run scrollkeeper-update after installation # XXX this runs while building the Debian package but does no harm import sys, os if 'install' in sys.argv: os.system('scrollkeeper-update -q') geximon-0.7.7/test/0000755000175000017500000000000010470723547013123 5ustar daviddavidgeximon-0.7.7/test/test.py0000755000175000017500000000216510470723547014463 0ustar daviddavid#!/usr/bin/env python """Geximon unit testing module.""" import unittest import doctest import sys sys.path.insert(0, '..') class TestWindows(unittest.TestCase): def test_createGEximonWindow(self): from geximon.geximon import GEximonWindow win = GEximonWindow() def test_createProcessWindow(self): from geximon.geximon import ProcessWindow from geximon.preferences import Preferences prefs = Preferences() w = ProcessWindow(prefs) def test_createPopupWindow(self): from geximon.widgets import PopupWindow w = PopupWindow("title", "text") class TestPreferences(unittest.TestCase): def test_createDialog(self): from geximon.preferences import Preferences, PreferencesDialog prefs = Preferences() dlg = PreferencesDialog(None, prefs) if __name__ == '__main__': suite = unittest.TestSuite() from geximon import exim suite.addTest(doctest.DocTestSuite(exim)) suite.addTest(unittest.makeSuite(TestWindows)) suite.addTest(unittest.makeSuite(TestPreferences)) unittest.TextTestRunner(verbosity=2).run(suite) geximon-0.7.7/translation/0000755000175000017500000000000010470723547014502 5ustar daviddavidgeximon-0.7.7/translation/Makefile0000644000175000017500000000030710470723547016142 0ustar daviddavidextract: pygettext2.3 -o geximon.pot ../geximon/*.py for f in `ls *.po`; do \ msgmerge -U $$f geximon.pot ; \ done compile: for f in `ls *.po`; do \ msgfmt -o $${f%.po}.mo $$f ; \ done geximon-0.7.7/translation/geximon.mo0000644000175000017500000000130410470723547016503 0ustar daviddavid l    (3:?DLUQp    %d %s in queue (%d frozen).%d %s selected.Message IDRecipientsSenderSizeTimemessagemessagesProject-Id-Version: geximon 0.6 POT-Creation-Date: Sat Mar 27 18:01:15 2004 Last-Translator: Gintautas Miliauskas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated-By: pygettext.py 1.5 %d %s eilėje (%d įšaldyta).Pažymėta %d %s.Žinutės IDGavėjaiSiuntėjasDydisLaikasžinutėžinučiųgeximon-0.7.7/translation/geximon.pot0000644000175000017500000002476310470723547016710 0ustar daviddavid# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "POT-Creation-Date: Fri Apr 9 19:07:42 2004\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: ENCODING\n" "Generated-By: pygettext.py 1.5\n" #: ../geximon/exim.py:45 msgid "Error: could not open the exim log file at `%s`!" msgstr "" #: ../geximon/exim.py:209 msgid "" "Error invoking `exim -bpr`:\n" msgstr "" #: ../geximon/exim.py:251 msgid "%d %s been %s." msgstr "" #: ../geximon/exim.py:266 msgid "Spawning a queue runner in the background." msgstr "" #: ../geximon/exim.py:291 ../geximon/widgets.py:461 msgid "removed" msgstr "" #: ../geximon/exim.py:297 msgid "frozen" msgstr "" #: ../geximon/exim.py:303 msgid "thawed" msgstr "" #: ../geximon/exim.py:317 ../geximon/exim.py:323 msgid "modified" msgstr "" #: ../geximon/exim.py:329 ../geximon/widgets.py:467 msgid "marked as delivered" msgstr "" #: ../geximon/exim.py:349 msgid "Invalid status line: " msgstr "" #: ../geximon/exim.py:402 ../geximon/widgets.py:247 msgid "Permission problems!" msgstr "" #: ../geximon/exim.py:404 msgid "No exim processes are currently running." msgstr "" #: ../geximon/exim.py:406 msgid "No output from exiwhat!" msgstr "" #: ../geximon/exim.py:416 msgid "Error processing exiwhat output line: " msgstr "" #: ../geximon/exim.py:418 ../geximon/widgets.py:251 msgid "processes" msgstr "" #: ../geximon/exim.py:419 ../geximon/widgets.py:251 msgid "process" msgstr "" #: ../geximon/exim.py:420 msgid "%d exim %s." msgstr "" #: ../geximon/geximon.py:99 msgid "A queue runner has been started." msgstr "" #: ../geximon/geximon.py:100 msgid "A new exim instance has been started in the background. It will process the queue and then terminate." msgstr "" #: ../geximon/geximon.py:113 msgid "Reject log" msgstr "" #: ../geximon/geximon.py:117 msgid "Exim configuration" msgstr "" #: ../geximon/geximon.py:146 msgid "/_Geximon" msgstr "" #: ../geximon/geximon.py:147 msgid "/Geximon/" msgstr "" #: ../geximon/geximon.py:148 msgid "/Geximon/_Preferences" msgstr "" #: ../geximon/geximon.py:151 msgid "/Geximon/" msgstr "" #: ../geximon/geximon.py:152 msgid "/Geximon/_Quit" msgstr "" #: ../geximon/geximon.py:154 msgid "/_Exim" msgstr "" #: ../geximon/geximon.py:155 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:156 msgid "/Exim/Spawn _Queue Runner" msgstr "" #: ../geximon/geximon.py:158 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:159 msgid "/Exim/Exi_grep" msgstr "" #: ../geximon/geximon.py:160 msgid "/Exim/Exim_stats" msgstr "" #: ../geximon/geximon.py:161 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:162 msgid "/Exim/See _Rejectlog" msgstr "" #: ../geximon/geximon.py:163 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:164 msgid "/Exim/Get _Configuration" msgstr "" #: ../geximon/geximon.py:165 msgid "/_View" msgstr "" #: ../geximon/geximon.py:166 msgid "/View/" msgstr "" #: ../geximon/geximon.py:167 msgid "/View/_Plots" msgstr "" #: ../geximon/geximon.py:169 msgid "/View/_Statusbar" msgstr "" #: ../geximon/geximon.py:171 msgid "/View/" msgstr "" #: ../geximon/geximon.py:172 msgid "/View/_Process List" msgstr "" #: ../geximon/geximon.py:174 msgid "/View/" msgstr "" #: ../geximon/geximon.py:175 msgid "/View/_Scroll log on new data" msgstr "" #: ../geximon/geximon.py:177 msgid "/_Help" msgstr "" #: ../geximon/geximon.py:178 msgid "/Help/" msgstr "" #: ../geximon/geximon.py:179 msgid "/Help/_Contents" msgstr "" #: ../geximon/geximon.py:180 msgid "/Help/_About" msgstr "" #: ../geximon/geximon.py:259 msgid "Exim processes" msgstr "" #: ../geximon/geximon.py:285 msgid "About gEximon" msgstr "" #: ../geximon/geximon.py:297 msgid "A GNOME monitor for the exim mail transport agent." msgstr "" #: ../geximon/geximon.py:301 msgid "Author" msgstr "" #: ../geximon/gtkhelpers.py:21 msgid "_None" msgstr "" #: ../geximon/gtkhelpers.py:24 msgid "_On characters" msgstr "" #: ../geximon/gtkhelpers.py:28 msgid "_On words" msgstr "" #: ../geximon/gtkhelpers.py:40 msgid "_Wrapping" msgstr "" #: ../geximon/gtkhelpers.py:83 msgid "/_File" msgstr "" #: ../geximon/gtkhelpers.py:84 msgid "/File/" msgstr "" #: ../geximon/gtkhelpers.py:85 msgid "/File/_Save As" msgstr "" #: ../geximon/gtkhelpers.py:87 msgid "/File/_Close" msgstr "" #: ../geximon/gtkhelpers.py:105 msgid "The file with that name already exists.If you continue, the contents of the file will be overwritten." msgstr "" #: ../geximon/gtkhelpers.py:120 msgid "An error has occured." msgstr "" #: ../geximon/gtkhelpers.py:121 msgid "" "The file at `%s` could not be written.\n" "Error: %s" msgstr "" #: ../geximon/gtkhelpers.py:125 msgid "Text saved successfully." msgstr "" #: ../geximon/gtkhelpers.py:126 msgid "The data was successfully saved to `%s`." msgstr "" #: ../geximon/preferences.py:69 msgid "Invalid setting for %r in %s:" msgstr "" #: ../geximon/preferences.py:83 msgid "Invalid wrapping mode specified in %s." msgstr "" #: ../geximon/preferences.py:162 msgid "Preferences" msgstr "" #: ../geximon/preferences.py:182 msgid "Paths" msgstr "" #: ../geximon/preferences.py:202 msgid "Exim _log directory:" msgstr "" #: ../geximon/preferences.py:204 msgid "_Name of main log:" msgstr "" #: ../geximon/preferences.py:206 msgid "Path to exim _binaries:" msgstr "" #: ../geximon/preferences.py:208 msgid "_Name of exim binary:" msgstr "" #: ../geximon/preferences.py:210 msgid "Use _sudo" msgstr "" #: ../geximon/preferences.py:216 msgid "Update intervals" msgstr "" #: ../geximon/preferences.py:230 msgid "seconds" msgstr "" #: ../geximon/preferences.py:235 msgid "Queue display:" msgstr "" #: ../geximon/preferences.py:237 msgid "Process display:" msgstr "" #: ../geximon/preferences.py:239 msgid "Plots:" msgstr "" #: ../geximon/preferences.py:242 msgid "User interface" msgstr "" #: ../geximon/preferences.py:245 msgid "Remember window size between sessions." msgstr "" #: ../geximon/preferences.py:261 msgid "Plot area height:" msgstr "" #: ../geximon/preferences.py:263 msgid "pixels" msgstr "" #: ../geximon/preferences.py:268 msgid "Popup messages" msgstr "" #: ../geximon/preferences.py:271 msgid "Confirm dangerous actions" msgstr "" #: ../geximon/preferences.py:276 msgid "Show a popup report after successful actions" msgstr "" #: ../geximon/widgets.py:38 msgid "geximon started at %s" msgstr "" #: ../geximon/widgets.py:206 msgid "_Refresh" msgstr "" #: ../geximon/widgets.py:213 msgid "SIG_TERM" msgstr "" #: ../geximon/widgets.py:216 msgid "SIG_KILL" msgstr "" #: ../geximon/widgets.py:220 msgid "_Signal" msgstr "" #: ../geximon/widgets.py:249 msgid "Process has already terminated." msgstr "" #: ../geximon/widgets.py:252 msgid "Signal %d sent to %s %s." msgstr "" #: ../geximon/widgets.py:280 msgid "Message ID" msgstr "" #: ../geximon/widgets.py:295 msgid "Sender" msgstr "" #: ../geximon/widgets.py:295 msgid "Size" msgstr "" #: ../geximon/widgets.py:295 msgid "Time" msgstr "" #: ../geximon/widgets.py:296 msgid "Recipients" msgstr "" #: ../geximon/widgets.py:386 msgid "Updating message list..." msgstr "" #: ../geximon/widgets.py:400 ../geximon/widgets.py:434 #: ../geximon/widgets.py:582 msgid "message" msgstr "" #: ../geximon/widgets.py:400 ../geximon/widgets.py:434 #: ../geximon/widgets.py:582 msgid "messages" msgstr "" #: ../geximon/widgets.py:402 msgid "%d %s in queue (%d frozen)." msgstr "" #: ../geximon/widgets.py:435 msgid "%d %s selected." msgstr "" #: ../geximon/widgets.py:462 msgid "The message(s) will be irreversibly deleted and no bounce messages will be sent." msgstr "" #: ../geximon/widgets.py:464 msgid "discarded" msgstr "" #: ../geximon/widgets.py:465 msgid "The message(s) will be deleted and a delivery error message will be sent to the sender." msgstr "" #: ../geximon/widgets.py:468 msgid "The recipients who have not yet been processed will not receive the message(s)." msgstr "" #: ../geximon/widgets.py:509 msgid "_Refresh list" msgstr "" #: ../geximon/widgets.py:514 msgid "_Body" msgstr "" #: ../geximon/widgets.py:515 msgid "Body of" msgstr "" #: ../geximon/widgets.py:516 msgid "_Headers" msgstr "" #: ../geximon/widgets.py:517 msgid "Headers of" msgstr "" #: ../geximon/widgets.py:518 msgid "_Message log" msgstr "" #: ../geximon/widgets.py:519 msgid "Log of" msgstr "" #: ../geximon/widgets.py:520 msgid "_Show" msgstr "" #: ../geximon/widgets.py:527 msgid "_ID" msgstr "" #: ../geximon/widgets.py:529 msgid "_Sender" msgstr "" #: ../geximon/widgets.py:531 msgid "_Recipients" msgstr "" #: ../geximon/widgets.py:533 msgid "_Exigrep" msgstr "" #: ../geximon/widgets.py:538 msgid "_Freeze" msgstr "" #: ../geximon/widgets.py:540 msgid "_Thaw" msgstr "" #: ../geximon/widgets.py:544 msgid "Re_move" msgstr "" #: ../geximon/widgets.py:546 msgid "_Attempt to deliver" msgstr "" #: ../geximon/widgets.py:549 msgid "_Give up" msgstr "" #: ../geximon/widgets.py:551 msgid "Mark as _delivered" msgstr "" #: ../geximon/widgets.py:556 msgid "_Change sender" msgstr "" #: ../geximon/widgets.py:558 msgid "Change the sender of " msgstr "" #: ../geximon/widgets.py:558 msgid "Sender address:" msgstr "" #: ../geximon/widgets.py:559 msgid "Add reci_pients" msgstr "" #: ../geximon/widgets.py:561 msgid "Add recipients to " msgstr "" #: ../geximon/widgets.py:561 msgid "Enter the new recipients:" msgstr "" #: ../geximon/widgets.py:584 msgid "The%s selected %s will be %s." msgstr "" #: ../geximon/widgets.py:593 ../geximon/widgets.py:627 msgid "Action successful." msgstr "" #: ../geximon/widgets.py:596 msgid "There were problems." msgstr "" #: ../geximon/widgets.py:608 ../geximon/widgets.py:624 msgid "An error occured." msgstr "" #: ../geximon/widgets.py:655 msgid "exigrep returned no data." msgstr "" #: ../geximon/widgets.py:664 msgid "Run exigrep" msgstr "" #: ../geximon/widgets.py:664 msgid "Search for:" msgstr "" #: ../geximon/widgets.py:669 msgid "Parse as a regular expression" msgstr "" #: ../geximon/widgets.py:671 ../geximon/widgets.py:704 msgid "Scan all logfiles" msgstr "" #: ../geximon/widgets.py:688 msgid "Empty output from exigrep." msgstr "" #: ../geximon/widgets.py:690 msgid "exigrep returned no data. You may want to refine your search terms or try a search on all the log files." msgstr "" #: ../geximon/widgets.py:701 msgid "Run eximstats" msgstr "" #: ../geximon/widgets.py:702 msgid "Arguments to eximstats:" msgstr "" geximon-0.7.7/translation/lt.mo0000644000175000017500000000130410470723547015454 0ustar daviddavid l    (3:?DLUQp    %d %s in queue (%d frozen).%d %s selected.Message IDRecipientsSenderSizeTimemessagemessagesProject-Id-Version: geximon 0.7 POT-Creation-Date: Fri Apr 9 19:07:42 2004 Last-Translator: Gintautas Miliauskas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated-By: pygettext.py 1.5 %d %s eilėje (%d įšaldyta).Pažymėta %d %s.Žinutės IDGavėjaiSiuntėjasDydisLaikasžinutėžinučiųgeximon-0.7.7/translation/lt.po0000644000175000017500000002505110470723547015464 0ustar daviddavid# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR ORGANIZATION # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: geximon 0.7\n" "POT-Creation-Date: Fri Apr 9 19:07:42 2004\n" "Last-Translator: Gintautas Miliauskas \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: pygettext.py 1.5\n" #: ../geximon/exim.py:45 msgid "Error: could not open the exim log file at `%s`!" msgstr "" #: ../geximon/exim.py:209 msgid "Error invoking `exim -bpr`:\n" msgstr "" #: ../geximon/exim.py:251 msgid "%d %s been %s." msgstr "" #: ../geximon/exim.py:266 msgid "Spawning a queue runner in the background." msgstr "" #: ../geximon/exim.py:291 ../geximon/widgets.py:461 msgid "removed" msgstr "" #: ../geximon/exim.py:297 msgid "frozen" msgstr "" #: ../geximon/exim.py:303 msgid "thawed" msgstr "" #: ../geximon/exim.py:317 ../geximon/exim.py:323 msgid "modified" msgstr "" #: ../geximon/exim.py:329 ../geximon/widgets.py:467 msgid "marked as delivered" msgstr "" #: ../geximon/exim.py:349 msgid "Invalid status line: " msgstr "" #: ../geximon/exim.py:402 ../geximon/widgets.py:247 msgid "Permission problems!" msgstr "" #: ../geximon/exim.py:404 msgid "No exim processes are currently running." msgstr "" #: ../geximon/exim.py:406 msgid "No output from exiwhat!" msgstr "" #: ../geximon/exim.py:416 msgid "Error processing exiwhat output line: " msgstr "" #: ../geximon/exim.py:418 ../geximon/widgets.py:251 msgid "processes" msgstr "" #: ../geximon/exim.py:419 ../geximon/widgets.py:251 msgid "process" msgstr "" #: ../geximon/exim.py:420 msgid "%d exim %s." msgstr "" #: ../geximon/geximon.py:99 msgid "A queue runner has been started." msgstr "" #: ../geximon/geximon.py:100 msgid "" "A new exim instance has been started in the background. It will process the " "queue and then terminate." msgstr "" #: ../geximon/geximon.py:113 msgid "Reject log" msgstr "" #: ../geximon/geximon.py:117 msgid "Exim configuration" msgstr "" #: ../geximon/geximon.py:146 msgid "/_Geximon" msgstr "" #: ../geximon/geximon.py:147 msgid "/Geximon/" msgstr "" #: ../geximon/geximon.py:148 msgid "/Geximon/_Preferences" msgstr "" #: ../geximon/geximon.py:151 msgid "/Geximon/" msgstr "" #: ../geximon/geximon.py:152 msgid "/Geximon/_Quit" msgstr "" #: ../geximon/geximon.py:154 msgid "/_Exim" msgstr "" #: ../geximon/geximon.py:155 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:156 msgid "/Exim/Spawn _Queue Runner" msgstr "" #: ../geximon/geximon.py:158 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:159 msgid "/Exim/Exi_grep" msgstr "" #: ../geximon/geximon.py:160 msgid "/Exim/Exim_stats" msgstr "" #: ../geximon/geximon.py:161 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:162 msgid "/Exim/See _Rejectlog" msgstr "" #: ../geximon/geximon.py:163 msgid "/Exim/" msgstr "" #: ../geximon/geximon.py:164 msgid "/Exim/Get _Configuration" msgstr "" #: ../geximon/geximon.py:165 msgid "/_View" msgstr "" #: ../geximon/geximon.py:166 msgid "/View/" msgstr "" #: ../geximon/geximon.py:167 msgid "/View/_Plots" msgstr "" #: ../geximon/geximon.py:169 msgid "/View/_Statusbar" msgstr "" #: ../geximon/geximon.py:171 msgid "/View/" msgstr "" #: ../geximon/geximon.py:172 msgid "/View/_Process List" msgstr "" #: ../geximon/geximon.py:174 msgid "/View/" msgstr "" #: ../geximon/geximon.py:175 msgid "/View/_Scroll log on new data" msgstr "" #: ../geximon/geximon.py:177 msgid "/_Help" msgstr "" #: ../geximon/geximon.py:178 msgid "/Help/" msgstr "" #: ../geximon/geximon.py:179 msgid "/Help/_Contents" msgstr "" #: ../geximon/geximon.py:180 msgid "/Help/_About" msgstr "" #: ../geximon/geximon.py:259 msgid "Exim processes" msgstr "" #: ../geximon/geximon.py:285 msgid "About gEximon" msgstr "" #: ../geximon/geximon.py:297 msgid "A GNOME monitor for the exim mail transport agent." msgstr "" #: ../geximon/geximon.py:301 msgid "Author" msgstr "" #: ../geximon/gtkhelpers.py:21 msgid "_None" msgstr "" #: ../geximon/gtkhelpers.py:24 msgid "_On characters" msgstr "" #: ../geximon/gtkhelpers.py:28 msgid "_On words" msgstr "" #: ../geximon/gtkhelpers.py:40 msgid "_Wrapping" msgstr "" #: ../geximon/gtkhelpers.py:83 msgid "/_File" msgstr "" #: ../geximon/gtkhelpers.py:84 msgid "/File/" msgstr "" #: ../geximon/gtkhelpers.py:85 msgid "/File/_Save As" msgstr "" #: ../geximon/gtkhelpers.py:87 msgid "/File/_Close" msgstr "" #: ../geximon/gtkhelpers.py:105 msgid "" "The file with that name already exists.If you continue, the contents of the " "file will be overwritten." msgstr "" #: ../geximon/gtkhelpers.py:120 msgid "An error has occured." msgstr "" #: ../geximon/gtkhelpers.py:121 msgid "" "The file at `%s` could not be written.\n" "Error: %s" msgstr "" #: ../geximon/gtkhelpers.py:125 msgid "Text saved successfully." msgstr "" #: ../geximon/gtkhelpers.py:126 msgid "The data was successfully saved to `%s`." msgstr "" #: ../geximon/preferences.py:69 msgid "Invalid setting for %r in %s:" msgstr "" #: ../geximon/preferences.py:83 msgid "Invalid wrapping mode specified in %s." msgstr "" #: ../geximon/preferences.py:162 msgid "Preferences" msgstr "" #: ../geximon/preferences.py:182 msgid "Paths" msgstr "" #: ../geximon/preferences.py:202 msgid "Exim _log directory:" msgstr "" #: ../geximon/preferences.py:204 msgid "_Name of main log:" msgstr "" #: ../geximon/preferences.py:206 msgid "Path to exim _binaries:" msgstr "" #: ../geximon/preferences.py:208 msgid "_Name of exim binary:" msgstr "" #: ../geximon/preferences.py:210 msgid "Use _sudo" msgstr "" #: ../geximon/preferences.py:216 msgid "Update intervals" msgstr "" #: ../geximon/preferences.py:230 msgid "seconds" msgstr "" #: ../geximon/preferences.py:235 msgid "Queue display:" msgstr "" #: ../geximon/preferences.py:237 msgid "Process display:" msgstr "" #: ../geximon/preferences.py:239 msgid "Plots:" msgstr "" #: ../geximon/preferences.py:242 msgid "User interface" msgstr "" #: ../geximon/preferences.py:245 msgid "Remember window size between sessions." msgstr "" #: ../geximon/preferences.py:261 msgid "Plot area height:" msgstr "" #: ../geximon/preferences.py:263 msgid "pixels" msgstr "" #: ../geximon/preferences.py:268 msgid "Popup messages" msgstr "" #: ../geximon/preferences.py:271 msgid "Confirm dangerous actions" msgstr "" #: ../geximon/preferences.py:276 msgid "Show a popup report after successful actions" msgstr "" #: ../geximon/widgets.py:38 msgid "geximon started at %s" msgstr "" #: ../geximon/widgets.py:206 msgid "_Refresh" msgstr "" #: ../geximon/widgets.py:213 msgid "SIG_TERM" msgstr "" #: ../geximon/widgets.py:216 msgid "SIG_KILL" msgstr "" #: ../geximon/widgets.py:220 msgid "_Signal" msgstr "" #: ../geximon/widgets.py:249 msgid "Process has already terminated." msgstr "" #: ../geximon/widgets.py:252 msgid "Signal %d sent to %s %s." msgstr "" #: ../geximon/widgets.py:280 msgid "Message ID" msgstr "Žinutės ID" #: ../geximon/widgets.py:295 msgid "Sender" msgstr "Siuntėjas" #: ../geximon/widgets.py:295 msgid "Size" msgstr "Dydis" #: ../geximon/widgets.py:295 msgid "Time" msgstr "Laikas" #: ../geximon/widgets.py:296 msgid "Recipients" msgstr "Gavėjai" #: ../geximon/widgets.py:386 msgid "Updating message list..." msgstr "" #: ../geximon/widgets.py:400 ../geximon/widgets.py:434 #: ../geximon/widgets.py:582 msgid "message" msgstr "žinutė" #: ../geximon/widgets.py:400 ../geximon/widgets.py:434 #: ../geximon/widgets.py:582 msgid "messages" msgstr "žinučių" #: ../geximon/widgets.py:402 msgid "%d %s in queue (%d frozen)." msgstr "%d %s eilėje (%d įšaldyta)." #: ../geximon/widgets.py:435 msgid "%d %s selected." msgstr "Pažymėta %d %s." #: ../geximon/widgets.py:462 msgid "" "The message(s) will be irreversibly deleted and no bounce messages will be " "sent." msgstr "" #: ../geximon/widgets.py:464 msgid "discarded" msgstr "" #: ../geximon/widgets.py:465 msgid "" "The message(s) will be deleted and a delivery error message will be sent to " "the sender." msgstr "" #: ../geximon/widgets.py:468 msgid "" "The recipients who have not yet been processed will not receive the message" "(s)." msgstr "" #: ../geximon/widgets.py:509 msgid "_Refresh list" msgstr "" #: ../geximon/widgets.py:514 msgid "_Body" msgstr "" #: ../geximon/widgets.py:515 msgid "Body of" msgstr "" #: ../geximon/widgets.py:516 msgid "_Headers" msgstr "" #: ../geximon/widgets.py:517 msgid "Headers of" msgstr "" #: ../geximon/widgets.py:518 msgid "_Message log" msgstr "" #: ../geximon/widgets.py:519 msgid "Log of" msgstr "" #: ../geximon/widgets.py:520 msgid "_Show" msgstr "" #: ../geximon/widgets.py:527 msgid "_ID" msgstr "" #: ../geximon/widgets.py:529 msgid "_Sender" msgstr "" #: ../geximon/widgets.py:531 msgid "_Recipients" msgstr "" #: ../geximon/widgets.py:533 msgid "_Exigrep" msgstr "" #: ../geximon/widgets.py:538 msgid "_Freeze" msgstr "" #: ../geximon/widgets.py:540 msgid "_Thaw" msgstr "" #: ../geximon/widgets.py:544 msgid "Re_move" msgstr "" #: ../geximon/widgets.py:546 msgid "_Attempt to deliver" msgstr "" #: ../geximon/widgets.py:549 msgid "_Give up" msgstr "" #: ../geximon/widgets.py:551 msgid "Mark as _delivered" msgstr "" #: ../geximon/widgets.py:556 msgid "_Change sender" msgstr "" #: ../geximon/widgets.py:558 msgid "Change the sender of " msgstr "" #: ../geximon/widgets.py:558 msgid "Sender address:" msgstr "" #: ../geximon/widgets.py:559 msgid "Add reci_pients" msgstr "" #: ../geximon/widgets.py:561 msgid "Add recipients to " msgstr "" #: ../geximon/widgets.py:561 msgid "Enter the new recipients:" msgstr "" #: ../geximon/widgets.py:584 msgid "The%s selected %s will be %s." msgstr "" #: ../geximon/widgets.py:593 ../geximon/widgets.py:627 msgid "Action successful." msgstr "" #: ../geximon/widgets.py:596 msgid "There were problems." msgstr "" #: ../geximon/widgets.py:608 ../geximon/widgets.py:624 msgid "An error occured." msgstr "" #: ../geximon/widgets.py:655 msgid "exigrep returned no data." msgstr "" #: ../geximon/widgets.py:664 msgid "Run exigrep" msgstr "" #: ../geximon/widgets.py:664 msgid "Search for:" msgstr "" #: ../geximon/widgets.py:669 msgid "Parse as a regular expression" msgstr "" #: ../geximon/widgets.py:671 ../geximon/widgets.py:704 msgid "Scan all logfiles" msgstr "" #: ../geximon/widgets.py:688 msgid "Empty output from exigrep." msgstr "" #: ../geximon/widgets.py:690 msgid "" "exigrep returned no data. You may want to refine your search terms or try a " "search on all the log files." msgstr "" #: ../geximon/widgets.py:701 msgid "Run eximstats" msgstr "" #: ../geximon/widgets.py:702 msgid "Arguments to eximstats:" msgstr ""