Initial import.
authorLars Wirzenius <liw@esme>
Sat, 3 Dec 2005 18:41:56 +0000 (20:41 +0200)
committerLars Wirzenius <liw@esme>
Sat, 3 Dec 2005 18:41:56 +0000 (20:41 +0200)
124 files changed:
COPYING [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
Makefile [new file with mode: 0644]
NEWS [new file with mode: 0644]
README [new file with mode: 0644]
enemies-of-carlotta [new file with mode: 0644]
enemies-of-carlotta.1 [new file with mode: 0644]
enemies-of-carlotta.1.es [new file with mode: 0644]
enemies-of-carlotta.1.fr [new file with mode: 0644]
eoc.py [new file with mode: 0644]
eocTests.py [new file with mode: 0644]
fix-config [new file with mode: 0755]
qmqp.py [new file with mode: 0644]
templates/bounce-goodbye [new file with mode: 0644]
templates/bounce-goodbye.es [new file with mode: 0644]
templates/bounce-goodbye.fi [new file with mode: 0644]
templates/bounce-goodbye.fr [new file with mode: 0644]
templates/bounce-goodbye.sv [new file with mode: 0644]
templates/bounce-owner-notification [new file with mode: 0644]
templates/bounce-owner-notification.es [new file with mode: 0644]
templates/bounce-owner-notification.fi [new file with mode: 0644]
templates/bounce-owner-notification.fr [new file with mode: 0644]
templates/bounce-owner-notification.sv [new file with mode: 0644]
templates/bounce-warning [new file with mode: 0644]
templates/bounce-warning.es [new file with mode: 0644]
templates/bounce-warning.fi [new file with mode: 0644]
templates/bounce-warning.fr [new file with mode: 0644]
templates/bounce-warning.sv [new file with mode: 0644]
templates/footer [new file with mode: 0644]
templates/footer.es [new file with mode: 0644]
templates/footer.fi [new file with mode: 0644]
templates/footer.fr [new file with mode: 0644]
templates/footer.sv [new file with mode: 0644]
templates/help [new file with mode: 0644]
templates/help.es [new file with mode: 0644]
templates/help.fi [new file with mode: 0644]
templates/help.fr [new file with mode: 0644]
templates/help.sv [new file with mode: 0644]
templates/list [new file with mode: 0644]
templates/list-sorry [new file with mode: 0644]
templates/list-sorry.es [new file with mode: 0644]
templates/list-sorry.fi [new file with mode: 0644]
templates/list-sorry.fr [new file with mode: 0644]
templates/list-sorry.sv [new file with mode: 0644]
templates/list.es [new file with mode: 0644]
templates/list.fi [new file with mode: 0644]
templates/list.fr [new file with mode: 0644]
templates/list.sv [new file with mode: 0644]
templates/msg-moderate [new file with mode: 0644]
templates/msg-moderate.es [new file with mode: 0644]
templates/msg-moderate.fi [new file with mode: 0644]
templates/msg-moderate.fr [new file with mode: 0644]
templates/msg-moderate.sv [new file with mode: 0644]
templates/msg-wait [new file with mode: 0644]
templates/msg-wait.es [new file with mode: 0644]
templates/msg-wait.fi [new file with mode: 0644]
templates/msg-wait.fr [new file with mode: 0644]
templates/msg-wait.sv [new file with mode: 0644]
templates/setlist-badlist [new file with mode: 0644]
templates/setlist-badlist.es [new file with mode: 0644]
templates/setlist-badlist.fi [new file with mode: 0644]
templates/setlist-badlist.fr [new file with mode: 0644]
templates/setlist-badlist.sv [new file with mode: 0644]
templates/setlist-confirm [new file with mode: 0644]
templates/setlist-confirm.es [new file with mode: 0644]
templates/setlist-confirm.fi [new file with mode: 0644]
templates/setlist-confirm.fr [new file with mode: 0644]
templates/setlist-confirm.sv [new file with mode: 0644]
templates/setlist-done [new file with mode: 0644]
templates/setlist-done.es [new file with mode: 0644]
templates/setlist-done.fi [new file with mode: 0644]
templates/setlist-done.fr [new file with mode: 0644]
templates/setlist-done.sv [new file with mode: 0644]
templates/setlist-sorry [new file with mode: 0644]
templates/setlist-sorry.es [new file with mode: 0644]
templates/setlist-sorry.fi [new file with mode: 0644]
templates/setlist-sorry.fr [new file with mode: 0644]
templates/setlist-sorry.sv [new file with mode: 0644]
templates/sub-confirm [new file with mode: 0644]
templates/sub-confirm.es [new file with mode: 0644]
templates/sub-confirm.fi [new file with mode: 0644]
templates/sub-confirm.fr [new file with mode: 0644]
templates/sub-confirm.sv [new file with mode: 0644]
templates/sub-moderate [new file with mode: 0644]
templates/sub-moderate.es [new file with mode: 0644]
templates/sub-moderate.fi [new file with mode: 0644]
templates/sub-moderate.fr [new file with mode: 0644]
templates/sub-moderate.sv [new file with mode: 0644]
templates/sub-owner-notification [new file with mode: 0644]
templates/sub-owner-notification.es [new file with mode: 0644]
templates/sub-owner-notification.fi [new file with mode: 0644]
templates/sub-owner-notification.fr [new file with mode: 0644]
templates/sub-owner-notification.sv [new file with mode: 0644]
templates/sub-reject [new file with mode: 0644]
templates/sub-reject.es [new file with mode: 0644]
templates/sub-reject.fi [new file with mode: 0644]
templates/sub-reject.fr [new file with mode: 0644]
templates/sub-reject.sv [new file with mode: 0644]
templates/sub-wait [new file with mode: 0644]
templates/sub-wait.es [new file with mode: 0644]
templates/sub-wait.fi [new file with mode: 0644]
templates/sub-wait.fr [new file with mode: 0644]
templates/sub-wait.sv [new file with mode: 0644]
templates/sub-welcome [new file with mode: 0644]
templates/sub-welcome.es [new file with mode: 0644]
templates/sub-welcome.fi [new file with mode: 0644]
templates/sub-welcome.fr [new file with mode: 0644]
templates/sub-welcome.sv [new file with mode: 0644]
templates/unsub-confirm [new file with mode: 0644]
templates/unsub-confirm.es [new file with mode: 0644]
templates/unsub-confirm.fi [new file with mode: 0644]
templates/unsub-confirm.fr [new file with mode: 0644]
templates/unsub-confirm.sv [new file with mode: 0644]
templates/unsub-goodbye [new file with mode: 0644]
templates/unsub-goodbye.es [new file with mode: 0644]
templates/unsub-goodbye.fi [new file with mode: 0644]
templates/unsub-goodbye.fr [new file with mode: 0644]
templates/unsub-goodbye.sv [new file with mode: 0644]
templates/unsub-owner-notification [new file with mode: 0644]
templates/unsub-owner-notification.es [new file with mode: 0644]
templates/unsub-owner-notification.fi [new file with mode: 0644]
templates/unsub-owner-notification.fr [new file with mode: 0644]
templates/unsub-owner-notification.sv [new file with mode: 0644]
testrun.py [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..d60c31a
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA  02111-1307  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.
+\f
+                   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.)
+\f
+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.
+\f
+  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.
+\f
+  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
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..9e5239e
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,720 @@
+2005-12-03  Lars Wirzenius  <liw@iki.fi>
+
+       * I'm switching EoC development to bzr and using commit messages
+       instead of a Changelog. This is the last ChangeLog entry.
+
+2005-12-03  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1: Changed - to \- since that is more
+       correct. Added the missing word "pipe" in the description
+       of --moderate. Added note about + vs - in the Postfix regexp
+       example. Documented --show-lists.
+       
+       * eoc.py: Don't append a footer if the message is base64 encoded.
+
+2005-04-16  Lars Wirzenius  <liw@iki.fi>
+
+       * templates/*: Changed content dispositions from attachment to
+         inline so that they are shown by default in mailers.
+
+2005-04-16  Lars Wirzenius  <liw@iki.fi>
+
+       * templates/bounce-owner-notification, templates/bounce-warning,
+         templates/setlist-confirm: Say "attached" instead of "appended"
+         or "below".
+
+2005-04-16  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: nice_7bit: Don't treat white space characters as
+          requiring MIME encoding.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.1.5.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py, templates/help*: Use "EoC" instead of "Enemies of
+        Carlotta" so as not to scare off users who don't get the movie
+        reference. Did not change filenames, since those are only
+        visible to the listmaster and the listmaster presumably can read
+        the home page.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Cleaning woman now removes groups without subscribers.
+       They can happen when we add a subscriber to a list when they are
+       there already and were the only member in their group.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, eocTests.py, enemies-of-carlotta.1: Added option 
+       "pristine-headers" to disable header MIME encoding.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+        * enemies-of-carlotta.1, eoc.py, eocTests.py,
+          templates/msg-moderate: Applied patches from Pascal Hakim
+          to implement post moderators separately from list owners.
+          If there are no moderators, the list owners are also moderators.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, eocTest.py, enemies-of-carlotta.1: Applied patch from
+       Jaakko Niemi to implement a feature to optionally ignore bounces.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, eocTests.py: Fixes for treating addresses in a
+       case in-sensitive manner (but storing in the form that the
+       user gave).
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Don't log debug() to stderr. Log info() and error() first
+       to file, and to stderr only after that. This should avoid problems
+       when MTA's buffer for our stderr output fills up.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+        * templates/sub-owner-notification.fi,
+          templates/unsub-owner-notification.fi: Added the missing word
+          "tehdä". Pointed out by Leena Romppainen.
+
+2005-04-10  Lars Wirzenius  <liw@iki.fi>
+
+        * Makefile: Installation improvements. Added a mandir variable
+        and made man1dir, man1dires, and man1dirfr dependent on it. This
+        allows the manual dirs to easily be relocated. Also added a
+        command variable to allow renaming of the command in bindir.
+
+2004-12-06  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.1.4.
+
+2004-12-06  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Introduced locking of individual messages to MessageBox.
+       Used this to prevent more than one request for post approval
+       processing from happening at the same time. This should fix the
+       problem found by Pascal Hakim, where several moderators are so
+       fast to reply to a moderation request that the list gets two or
+       more copies of the same message.
+
+2004-12-06  Lars Wirzenius  <liw@iki.fi>
+
+       * TODO: Added.
+
+2004-12-05  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1.es: Updated Spanish manual page from
+       Ricardo Javier Cardenes.
+
+2004-12-04  Lars Wirzenius  <liw@iki.fi>
+
+       * README.CVS: Wrote.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * Making release version 1.1.3.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: If email.Header module is missing, then work as otherwise,
+        but don't MIME encode headers. This should restore the ability to
+        run on Python 2.1 and 2.2.
+        
+        * README: Noted.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: --show-lists now alphabetizes the output.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * templates/bounce-probe*, templates/sub-already*,
+          templates/unsub-already*: Unused, removed.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: Added the beginnings of a plugin feature. The only hook
+        for now is "send_mail_to_subscribers_hook" which allows the plugin
+        to manipulate the mail before it is sent to the subscribers.
+        
+        * eoc.py: Added --no-act option to make testing easier.
+        
+        * enemies-of-carlotta.1: Documented the plugins.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: Made it so that an empty template means the mail is not
+        sent at all. This can be used to prevent, say, "please wait for
+        moderation" messages from being sent on a per-list basis.
+        
+        * enemies-of-carlotta.1: Documented this.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: Added --version option.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: Allow setting of more list options with --create or
+        --edit: added new options --language, --mail-on-forced-unsubscribe,
+        --mail-on-subscription-changes.
+        
+        * enemies-of-carlotta.1: Documented them.
+
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: Added command line options --get and --set.
+        
+        * enemies-of-carlotta.1: Documented them.
+        
+2004-11-27  Lars Wirzenius  <liw@iki.fi>
+
+        * eocTests.py: Added a test to check that the headers of sent
+        mails are only 7-bit ASCII characters (tab, newline, carriage
+        return, plus 32-126).
+        
+        * eoc.py: Encode outgoing mails to use only 7-bit characters in
+        the headers. This is required for the mails to work correctly
+        with MIME compliant mail readers. Note that this change makes
+        use of the email.Header module in Python 2.3, so with this change
+        the minimum supported Python version is 2.3.
+        
+        * README: Noted Python version requirement change.
+
+2004-09-18  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.1.2.
+
+2004-09-18  Lars Wirzenius  <liw@iki.fi>
+
+        * enemies-of-carlotta.1.es: Added manual page translation to
+        Spanish by Ivan Juanes. Nobody expectes the Spanish manual
+        page.
+       
+       * Makefile: Install the Spanish manual page.
+
+2004-09-18  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, templates/*: Implemented suggestion from Magnus Holmgren
+       to make attached messages real MIME attachments rather than 
+       just inserting the raw message code into a text/plain message.
+       Also converted all templates to UTF-8, since that makes it 
+       easier to edit them for me. Hopefully nothing broke.
+
+2004-09-18  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: When an EoC exception occurs, print out a sensible
+       error message about it rather than letting Python print out
+       a stack trace.
+
+2004-09-18  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py, eocTests.py, enemies-of-carlotta.1: Added support for
+        adding headers to and removing headers from mails sent to the
+        list. See the files headers-to-add and headers-to-remove in the
+        list directories.
+
+2004-09-12  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, eocTest.py: Added support for $listdir/headers-to-add
+       (but it still needs to be documented).
+
+2004-09-03  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, eocTest.py: Some refactoring and new unit tests.
+
+2004-08-28  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.1.1. This release is dedicated to Jaakko
+       Niemi, the winner of the bug finding competition for 1.1.0.
+
+2004-08-28  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1: Added note about the "templates" directory
+       in the list specific directory using a patch sent by Jaakko Niemi,
+       who thereby won the bug finding competition.
+
+2004-08-26  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: in AddressParser, return the canonical name of the list,
+       instead of converting it to lower case. Also, when opening a list,
+       open using the canonical name rather than one converted to lower
+       case or derived from the incoming mail address. Thanks to Jaakko
+       Niemi for pointing these problems out.
+       
+       * eocTests.py: Improved testing cases when the name of a list is
+       not all lower case.
+
+2004-08-26  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, eocTests.py: Bugfix for bug found by Jaakko Niemi.
+       If the list has been created with a name containing upper case,
+       --is-list won't work. Fixed by making AddressParser convert
+       all list names it gets to lower case.
+
+       * Makefile: Bugfix for bug found by Jaakko Niemi. Used bashism in
+       the install target, which made install not work under dash and
+       other shells. Fixed by manually expanding a {foo,bar} construct.
+       
+       * Makefile: Bugfix for bug found by Jaakko Niemi. qmqp.py was not
+       installed by "make install". Oops.
+
+2004-08-24  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Changed md5sum_as_hex to use .hexdigest() instead of
+       doing the hex conversion manually. I should read more manuals.
+       Thanks to Magnus Holmgren for pointing this out.
+
+2004-08-23  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.1.0.
+
+2004-07-25  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: Added a realname for the From line in message
+        templates. This is meant to avoid some spam filters.
+
+2004-07-25  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py, eocTests.py: Refactoring. Moved address parsing into
+        its own class, for simplicity, and started work on making EoC
+        specific exceptions be more user friendly as far as error
+        messages are concerned.
+
+2004-07-25  Lars Wirzenius  <liw@iki.fi>
+
+        * eocTests.py: Added some test cases for recipient address
+          parsing.
+
+2004-07-09  Lars Wirzenius  <liw@iki.fi>
+
+        * eoc.py: Refactoring changes to make code nicer. No functional
+        changes.
+
+2004-07-09  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Removed a bunch of documentation from the beginning of
+       the file. It was never finished, and was partly outdated, and it's
+       better put in README anyway, when I have a moment to flesh it out.
+       
+       * eocTests.py: Simplified (shortened) the implementation of a test
+       case in the anticipation of adding more cases to it.
+
+2004-03-31  Lars Wirzenius  <liw@iki.fi>
+
+        * templates/*.sv: Added Swedish translation from Magnus Holmgren.
+
+2004-02-21  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Use the rejection address for subscription and posting
+       moderation requests. This is to make it easier for mutt users
+       to mail to the rejection address: they can just answer "no"
+       to the question about using Reply-To. Thanks to Antti-Juhani
+       Kaijanaho for pointing this out.
+
+2004-02-21  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1: Updated.
+       
+       * eoc.py: Added --sender and --recipient options on suggestion
+       from Tommi Virtanen.
+
+2004-02-21  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Convert addresses to lower case so that random
+       capitalizations (especially in domain names) don't break things
+       for us.
+
+2004-02-21  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Added some safeguards against subscribing addresses
+       without @ characters.
+
+2004-02-21  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Catch GetoptError exception and print an error message.
+       This is nicer for the user than the stack trace.
+
+2004-02-21  Lars Wirzenius  <liw@iki.fi>
+
+       * qmqp.py: Added QMQP sending module by Jaakko Niemi. Thanks!
+       Did change encoding of single recipient so that Postfix will
+       accept that.
+       
+       * eoc.py: Changes to allow use of QMQP. Also, when logging a
+       sent mail, do it with a bit more white space so that it will be
+       easier to read the log file.
+
+2004-01-13  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1: Removed the word "also" from the
+       description of where subscription confirmation requests are
+       sent, since it was incorrect. Also added a suggestion that
+       --cleaning-woman should be run once per hour.
+       
+       * Makefile: Added patch from Jacek Konieczny to add DESTDIR
+       support.
+
+2003-09-07  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.0.3.
+
+2003-09-07  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1: Removed erroneous quotation marks in
+       the Qmail section. Added section documenting all mail commands.
+
+2003-09-06  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Bugfix. log_file() was broken in that it would replace
+       an already open output stream with one writing to /dev/null.
+       Fixed that. Also, not writing to /dev/null anymore in cases where
+       DOTDIR doesn't exist, but rather using a special purpose output
+       file stream simulator DevNull.
+
+2003-07-16  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py, eocTests.py, enemies-of-carlotta.1: Added command line
+       option --post for bypassing moderation status on a list.
+
+2003-07-14  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.0.2.
+
+2003-06-20  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1: Fixed the example for creating a
+       mailing list.
+       
+       * eoc.py: Added option --show-lists, from Stefan (who gave no
+       last name).
+
+2003-05-11  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.0.1.
+
+2003-05-11  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Bounce handling was totally broken, because the
+       final step was missing. What used to happen: EoC sends mail to
+       subscribers, it bounces, states goes from "ok" to "bounced";
+       after one week, cleaning woman sends probe; two weeks after
+       bounce, cleaning woman unsubscribes. What was missing: if probe
+       bounces, state is set to "probebounced", and cleaning woman only
+       unsubscribes if state is "probebounced", otherwise it resets
+       state to "ok". This was quite an embarrassing bug.
+
+2003-05-11  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: --help option implemented.
+       * eoc.py: Don't create dotdir if only --help is given.
+       * eoc.py: If dotdir exists, but secret doesn't, create secret
+       instead of crashing.
+
+2003-04-13  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 1.0.
+       
+       * There have been no changes. I had planned to improve
+       documentation, but the release party is tomorrow and, well,
+       I want this out. Anyway, I'll have a better idea what to put
+       into the manual if people first send me hate mail about the
+       difficult parts.
+
+2003-04-13  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 0.23.
+
+2003-04-13  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: -reject now actually removes the rejected message. Oops.
+
+2003-04-13  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Changed mail command -setlist to welcome new subscribers
+       and say goodbye to old ones.
+
+       * eoc.py: Added mail command -setlistsilently, which is the same
+       as -setlist, but preserves the old behavior of not welcoming or
+       saying goodbye.
+       
+       * templates/setlist-confirm{,.es,.fi,.fr}: Removed the sentence
+       saying that new subscribers won't be welcomed. Hopefully I
+       recognized it correctly in French and Spanish.
+
+2003-03-22  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release 0.22.
+
+2003-03-22  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Messages sent by EoC that used to have an empty SMTP
+       sender (which made them look like bounce messages) now have
+       foo-ignore@example (for the foo@example.com list). This avoids
+       having to deal with different ways to specify an empty sender
+       and is also more correct since the messages sent by EoC aren't
+       really bounces.
+
+2003-03-16  Lars Wirzenius  <liw@iki.fi>
+
+       * Making release version 0.21.
+
+2003-03-15  Lars Wirzenius  <liw@iki.fi>
+
+       * templates/*.es and *.fr: Translations to Spanish for new
+       templates by Ricardo Javier Cardenes, and to French by Pierre
+       Machard.
+
+2003-03-15  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Implemented -setlist command to allow list owner to
+       change the whole subscriber list as one operation. This should
+       be useful for people maintaining the list of subscriber outside
+       EoC's control, e.g., when the list is generated from a database.
+       
+       * templates/setlist-badlist, templates/setlist-confirm,
+       templates/setlist-done, templates/setlist-sorry: New templates
+       for this feature. Need to be translated.
+       
+       * templates/setlist-*.fi: Translated to Finnish.
+
+2003-03-15  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: If the sender of a subscription or unsubscription
+       request is a list owner, the list owners are requested to do
+       the confirmation, instead of the address being subscribed.
+       The welcome or goodbye message is still sent to the subscriber.
+
+2003-03-15  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: When someone is sending mail to a moderated list,
+       inform them that their message has been sent to the list owners
+       for approval.
+       
+       * templates/msg-wait: New mail template. Needs translations.
+       
+       * templates/msg-wait.fi: Translated to Finnish.
+
+2003-03-15  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1.fr: Proofread translation by Gérard
+       Delafond, sent by Pierre Machard.
+
+2003-03-15  Lars Wirzenius  <liw@iki.fi>
+
+       * templates/footer.es, templates/footer.es: Translations re-worded
+       so that they only use 7 bit ASCII characters, to avoid charset
+       problems. Thanks to Pierre Machard and Ricardo Javier Cardenes.
+
+2003-03-14  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: When subscribing to a list with subscription moderation,
+       the would-be subscriber is notified that a request has been sent
+       to the moderator and that they need to be patient.
+       
+       * eocTests.py: Related changes.
+
+       * templates/sub-wait: New mail template for this. Needs
+       translation.
+       
+       * templates/sub-wait.fi: Translated sub-wait to Finnish.
+       
+       * templates/*.fr: Added Content-type headers to all templates.
+       
+2003-03-14  Lars Wirzenius  <liw@iki.fi>
+
+       * templates/footer.fi: Reworded to use only us-ascii letters.
+
+2003-03-08  Lars Wirzenius  <liw@iki.fi>
+
+       * BENCHMARKS, eoc-benchmark, eoc-benchmark-procmailrc: Added.
+
+2003-02-25  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta.1.fr, templates/*.fr: Added translations
+       sent by Pierre Machard (pierre at machard.org).
+       
+       * templates/*.es: Added translations sent by Ricardo Javier
+       Cardenes (ricardo at conysis.com).
+       
+       * Making release 0.20.
+
+2003-02-22  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Added configuration option "language".
+       
+       * templates/*.fi: Translated templates to Finnish so that I can
+       test the "language" configuration option.
+
+       * enemies-of-carlotta.1: Added "CONFIGURATION" section.
+       
+       * Making relase 0.19.
+
+2003-02-18  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Use os.path.isfile instead of os.path.exists to see
+       whether a MessageBox contains a file.
+
+2003-02-16  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Command line options --edit, --subscribe, --unsubscribe,
+       and --list now allow the list name to be abbreviated by
+       leaving out the domain (and @). I don't want to allow shorter
+       abbreviations to make it less likely that you specify the wrong
+       list by mistake.
+       
+       * eoc.py: When a bouncing address is restored to "ok" status,
+       its bounce message is removed from the bounce-box.
+       
+       * eoc.py: Added configuration options mail-on-forced-unsubscribe
+       and mail-on-subscription-changes.
+       
+       * Making release 0.18.
+
+2003-02-09  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Applied patch from Ricardo Javier Cardenes
+       <ricardo@conysis.com> to implement posting option "auto", which
+       will let messages from subscribers automatically into the list
+       and send others to the moderator.
+       
+       * Making release 0.17.
+
+2003-01-12  Lars Wirzenius  <liw@iki.fi>
+
+       * Releasing version 0.16.1.
+
+       * eoc.py: Bounce message quoting had a stupid bug: it didn't 
+       add newlines.
+
+2003-01-11  Lars Wirzenius  <liw@iki.fi>
+
+       * Releasing version 0.16.
+
+       * eoc.py: First bounce message is now saved (up to 4096 bytes) and
+       quoted in the bounce-warning message.
+
+       * eoc.py: When state changes to bounce, it is noted in the log file.
+
+       * eoc.py, eocTests.py: Addresses now can't be added twice to
+       the list.
+
+       * eoc.py: Added missing exception MissingTemplate.
+       
+2002-12-11  Lars Wirzenius  <liw@iki.fi>
+
+       * Releasing version 0.15.
+
+2002-12-08  Lars Wirzenius  <liw@iki.fi>
+
+       * Added --moderate option for asking a message to be
+       moderated. To be used for spam filtering, at least.
+
+2002-12-08  Lars Wirzenius  <liw@iki.fi>
+
+       * eocTests.py: Set the quiet flag, so that "make check" doesn't
+       output debuggning messages.
+
+2002-12-08  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Added --smtp-server option for sending via SMTP, not
+       /usr/sbin/sendmail.
+       
+       * enemies-of-carlotta.1: Documented all options.
+       
+       * eocTests.py: Don't require a dot-eoc directory for running
+       the tests.
+
+2002-12-08  Lars Wirzenius  <liw@iki.fi>
+
+       * Releasing version 0.14.
+
+2002-12-08  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Also look in the list's template directory for templates.
+       This is useful for doing list specific customizations, e.g., for
+       the footer.
+
+2002-12-07  Lars Wirzenius  <liw@iki.fi>
+
+       * enemies-of-carlotta: Wrote startup wrapper for faster startup.
+       
+       * Makefile: Install startup wrapper as the binary, and eoc.py
+       into the share directory, plus compile eoc.py on installtion.§
+
+2002-11-04  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Cleaning woman now logs addresses it removes and sends
+       a final goodbye message when it does.
+
+       * templates/bounce-probe: Fixed spelling mistake.
+
+       * templates/bounce-goodbye: Wrote.
+
+2002-10-26  Lars Wirzenius  <liw@iki.fi>
+
+       * Added support for a footer to be appended to each mail.
+
+2002-10-26  Lars Wirzenius  <liw@iki.fi>
+
+       * A bounce splits a group with many addresses. If the original group
+       contained addresses in several domains, it is split into groups 
+       according to domains, otherwise into groups with single addresses.
+
+       * --cleaning-woman joins groups that haven't bounced for a week
+       into bigger groups.
+
+2002-10-26  Lars Wirzenius  <liw@iki.fi>
+
+       * Releasing version 0.13.
+
+2002-10-26  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Bug fix. When there were over ten subscribers, the
+       generation of group ids was wrong, since a list of group ids
+       was sorted lexically, instead of numerically.
+       
+       * eocTests.py: Added test case to test this.
+
+2002-10-26  Lars Wirzenius  <liw@iki.fi>
+
+       * eoc.py: Added --quiet and --sendmail options.
+       
+       * eocTests.py: Related changes.
+
+2002-10-21  Lars Wirzenius  <liw@iki.fi>
+
+       * Releasing version 0.12.
+
+2002-10-21  Lars Wirzenius  <liw@iki.fi>
+
+       * --incoming always reads stdin.
+
+2002-10-11  Lars Wirzenius  <liw@iki.fi>
+
+       * Makefile: added $(prefix), installation of templates.
+       
+       * fix-config: directory is given on the command line.
+       
+       * eoc.py: Added some more debugging output.
+
+2002-09-22  Lars Wirzenius  <liw@iki.fi>
+
+       * Starting upstream ChangeLog. Sooner or later someone else is
+         going to be maintaining the Debian packaging stuff, and then
+         it's sensible to keep things separate.
+
+       * Removed debian/*. I can live without them myself, and I don't
+         want to maintain them when there are perfectly too Debian
+         developers available. :)
+
+       * Makefile: Added an install target. Very simplistic.
+       
+       * COPYING, README: Added these files. This is beginning to seem
+         like something that can be shown in public.
+
+       * Releasing version 0.11.
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..5420849
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,31 @@
+prefix = /usr/local
+bindir = $(prefix)/bin
+mandir = $(prefix)/share/man
+man1dir = $(mandir)/man1
+man1dires = $(mandir)/es/man1
+man1dirfr = $(mandir)/fr/man1
+sharedir = $(prefix)/share/enemies-of-carlotta
+command = enemies-of-carlotta
+
+all: check
+
+check:
+       python testrun.py
+
+install:
+       install -d $(DESTDIR)$(bindir)
+       install -d $(DESTDIR)$(sharedir)
+       install -d $(DESTDIR)$(man1dir)
+       install -d $(DESTDIR)$(man1dires)
+       install -d $(DESTDIR)$(man1dirfr)
+       sed 's,^SHAREDIR=.*,SHAREDIR="$(sharedir)",' enemies-of-carlotta \
+           > $(DESTDIR)$(bindir)/$(command)
+       chmod 755 $(DESTDIR)$(bindir)/$(command)
+       sh fix-config $(sharedir) < eoc.py > $(DESTDIR)$(sharedir)/eoc.py
+       install -m 0755 qmqp.py $(DESTDIR)$(sharedir)/qmqp.py
+       python -c 'import py_compile; py_compile.compile("$(DESTDIR)$(sharedir)/eoc.py")'
+       python -c 'import py_compile; py_compile.compile("$(DESTDIR)$(sharedir)/qmqp.py")'
+       install -m 0644 enemies-of-carlotta.1 $(DESTDIR)$(man1dir)/$(command).1
+       install -m 0644 enemies-of-carlotta.1.es $(DESTDIR)$(man1dires)/$(command).1
+       install -m 0644 enemies-of-carlotta.1.fr $(DESTDIR)$(man1dirfr)/$(command).1
+       install -m 0644 templates/????* $(DESTDIR)$(sharedir)
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..6b92302
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,131 @@
+NEWS file for Enemies of Carlotta, a mailing list manager
+
+Significant user-visible changes from version 1.1.4 to version 1.1.5:
+
+    * Debug logs no longer go to stderr, just to the log file. This should
+      prevent sudden deaths from MTA buffers becoming full.
+      
+    * Bugs about treating addresses in case-insensitive manners fixed.
+    
+    * Lists can now be configured to ignore bounces. (By Jaakko Niemi.)
+    
+    * Moderators for posting can be separate from list owners. (By Pascal
+      Hakim.)
+      
+    * Header MIME encoding can be disabled per list.
+
+Significant user-visible changes from version 1.1.3 to version 1.1.4:
+
+    * Fixed race condition when multiple moderators approve or reject the
+      same message at the same time. This could result in the message being
+      sent multiple times.
+
+Significant user-visible changes from version 1.1.2 to version 1.1.3:
+
+    * Mails sent by EoC now get their headers MIME encoded (if the version
+      of Python in use has the email.Header module).
+
+    * A bunch of new command line options: --get, --set, --language,
+      --mail-on-forced-unsubscribe, --mail-on-subscription-changes,
+      --version
+      
+    * Making an empty template file prevents the corresponding mail from
+      being sent at all. This can be used, for example, to prevent
+      "please wait for moderation" mails.
+      
+    * The beginnings of a plugin architecture, where plugins can modify
+      EoC's operation or the mails it sends. There is only one supported
+      hook at the moment, though, but more will be added as needed.
+      
+    * --show-lists now shows lists in alphabetical order.
+
+Significant user-visible changes from version 1.1.1 to version 1.1.2:
+
+    * This is still a DEVELOPMENT release. There have been many changes
+      and the documentation and translations are probably out of date.
+      There may be bugs lurking as well.
+      
+    * Headers of messages sent to the list can be manipulated in simple
+      ways: some headers can be removed and others added.
+      
+    * Error messages are now user-friendlier than Python's stack traces.
+    
+    * When EoC attaches a message to something it sends to a user, it
+      does so by making it a proper MIME attachment.
+      
+    * Message templates are now in UTF-8.
+    
+    * Manual page in Spanish.
+
+Significant user-visible changes from version 1.1.0 to version 1.1.1:
+
+    * This is still a DEVELOPMENT release. There have been many changes
+      and the documentation and translations are probably out of date.
+      There may be bugs lurking as well.
+      
+    * Manual page mentions the "templates" sub-directory of the list specific
+      directory.
+      
+    * QMQP implementation is now installed by "make install".
+    
+    * Bashism in the Makefile have been removed.
+    
+    * Bugs regarding lists with names that are not all lower case have
+      been fixed.
+
+    * This release is dedicated to Jaakko Niemi who found most bugs in 1.1.0.
+
+Significant user-visible changes from version 1.0.3 to version 1.1.0:
+
+    * This is a DEVELOPMENT release. There have been many changes and the
+      documentation and translations are probably out of date. There may be
+      bugs lurking as well.
+      
+    * QMQP support from Jaakko Niemi.
+    
+    * Command line syntax errors result in an error message rather than 
+      a stack trace.
+
+    * Upper and lower case are no longer treated as being different characters
+      in e-mail addresses.
+      
+    * New options --sender and --recipient.
+    
+    * Subscription and posting moderation requests now use the rejection
+      address to make life easier for mutt users.
+      
+    * Swedish translation.
+    
+    * Mails sent by EoC now have a "real name" in the From header, to lessen
+      the chance they are caught by spam filters.
+
+Significant changes from version 1.0.2 to version 1.0.3:
+
+    * Added option --post, to bypass moderation.
+    
+    * The manual page now documents all mail command addresses.
+    
+    * Bug fix: Qmail section in the manual page had extra quotation marks.
+    They have been removed.
+    
+    * Bug fix: Log file didn't get more than the first message of each
+    run. Now fixed so that it gets all messages. (Stderr got everything
+    even before, though.)
+
+Significant changes from version 1.0.1 to version 1.0.2:
+
+    * Manual page fix: the example for creating a new list now actually
+    works.
+    
+    * Added option --show-lists, to list all lists.
+
+Significant changes from version 1.0 to version 1.0.1:
+
+    * Bug fixes only.
+    
+    * --help option implemented.
+    
+    * --create: If ~/.enemies-of-carlotta exists, but a file named
+    "secret" inside it doesn't, create the file instead of crashing.
+    
+    * Bounce handling fixed.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..6d2e4af
--- /dev/null
+++ b/README
@@ -0,0 +1,18 @@
+README for Enemies of Carlotta, a mailing list manager
+by Lars Wirzenius, liw@iki.fi
+
+
+Enemies of Carlotta is a simple mailing list manager that mimicks the
+ezmlm (http://www.ezmlm.org/) commands. Please see the manual page
+or the home page (http://liw.iki.fi/liw/eoc/) for documentation, and
+join the mailing list (send mail to eoc-subscribe@liw.iki.fi).
+
+You need Python 2.3 or newer, and lockfile (from procmail). If you are
+willing to live without having headers encoded with MIME, Python 2.1 or
+2.2 should also work.
+
+To install, edit Makefile, the variables bindir and man1dir, and
+then run "make install" as root.
+
+Enemies of Carlotta is licensed using the GNU General Public License,
+version 2.
diff --git a/enemies-of-carlotta b/enemies-of-carlotta
new file mode 100644 (file)
index 0000000..4852c46
--- /dev/null
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+#
+# This file works as the Enemies of Carlotta startup wrapper. The real
+# program is in eoc.py, stored in SHAREDIR (see below). It is large enough
+# that the time Python takes to parse and compile it to bytecode is
+# significant, therefore we use a very short wrapper (this file) and
+# install eoc.py in a way that allows it to be compiled, thus reducing
+# startup time.
+
+SHAREDIR="."
+
+import sys
+sys.path.insert(0, SHAREDIR)
+
+import eoc
+eoc.main(sys.argv[1:])
diff --git a/enemies-of-carlotta.1 b/enemies-of-carlotta.1
new file mode 100644 (file)
index 0000000..322ca65
--- /dev/null
@@ -0,0 +1,606 @@
+.TH ENEMIES\-OF\-CARLOTTA 1
+.SH NAME
+enemies\-of\-carlotta \- a simple mailing list manager
+.SH SYNOPSIS
+.B enemies\-of\-carlotta 
+.IR "" [ options "] [" addresses ]
+.SH "DESCRIPTION"
+.B enemies\-of\-carlotta
+is a simple mailing list manager.
+If you don't know what a mailing list manager is, you should learn
+what they are before trying to use one.
+A manual page is unfortunately too short to explain it.
+.PP
+Enemies of Carlotta keeps all data about the lists in the
+.I ~/.enemies\-of\-carlotta
+directory.
+It will be created automatically when you create your first list.
+You need to arrange manually for the mails to be processed by the
+list manager.
+The details differ from mail system to another.
+For QMail and Postfix, see below.
+.PP
+Each list has one or more owners, who also moderate subscriptions or
+moderate some or all postings.
+On completely unmoderated lists the list owners are responsible for
+answering questions about the list.
+On completely moderated lists, they have to approve each message before
+it is sent to the list.
+On lists with 
+.IR posting=auto ,
+messages from subscribers are sent automatically to the list, and the
+moderators have to approve the rest.
+.SH OPTIONS
+.TP
+.BR \-\-name= foo@example.com
+Specify the list the command is to operate on.
+Most of the remaining options require you to set the list name with this
+option.
+With the \-\-edit, \-\-subscribe, \-\-unsubscribe, and \-\-list options,
+the name can be abbreviated to by leaving out the @ sign and domain.
+.TP
+.BI \-\-create
+Create a new list.
+You must specify at least one owner with
+.BR \-\-owner .
+.TP
+.BI \-\-owner= address
+Specify a list owner when creating or editing a list.
+.TP
+.BI \-\-moderator= address
+Specificy a list moderator when creating or editing a list.
+.TP
+.BI \-\-language= language\-code
+Set the language code used for looking up template files.
+The code should be empty (the default, meaning English), or a two\-letter
+code such as 
+.B fi
+or
+.BR es .
+.TP
+.B \-\-cleaning\-woman
+Deal with bouncing addresses and do other cleanup.
+You must run
+.B "enemies\-of\-carlotta \-\-cleaning\-woman"
+periodically, such as once per hour.
+It will clean up all your lists.
+.TP
+.BI \-\-destroy
+Destroy the list.
+.TP
+.BI \-\-edit
+Modify the list configuration.
+.TP
+.BI \-\-subscription= type
+When creating a list, set its subscription mode to
+.I free
+or
+.IR moderated .
+Use with
+.BR \-\-edit ,
+or
+.BR \-\-create .
+.TP
+.BI \-\-posting= type
+When creating a list, set its posting mode to
+.IR free ,
+.IR auto ,
+or
+.IR moderated .
+Use with
+.BR \-\-edit ,
+or
+.BR \-\-create .
+.TP
+.BI \-\-archived= yes\-or\-no
+Should list messages be archived to the
+.B archive\-box
+directory in the list directory under the
+.B "~/.enemies\-of\-carlotta"
+directory.
+Use
+.I yes
+or
+.IR no .
+.TP
+.BI \-\-mail\-on\-subscription\-changes= yes\-or\-no
+Should the list owners be notified when someone subscribes to or
+unsubscribes from the list?
+Use
+.I yes
+or
+.IR no .
+Default is no.
+.TP
+.BI \-\-mail\-on\-forced\-unsubscription= yes\-or\-no
+Should list owners be notified when someone is forcibly dropped from
+the list due to too much bouncing?
+Use
+.I yes
+or
+.IR no .
+Default is no.
+.TP
+.BI \-\-ignore\-bounce= yes\-or\-no
+Should bounces be ignored?
+Use
+.I yes
+or
+.IR no .
+Default is no.
+.TP
+.BI \-\-list
+List the subscribers of a list.
+.TP
+.BI \-\-subscribe
+Add subscribers to a list.
+The non\-option arguments are the addresses to be subscribed.
+Note that addresses added this way won't be sent confirmation requests.
+.TP
+.BI \-\-unsubscribe
+Remove subscribers from a list.
+The non\-option arguments are the addresses to be unsubscribed.
+Note that addresses removed this way won't be sent confirmation requests.
+.TP
+.B \-\-incoming
+Deal with an incoming message in the standard input.
+The SMTP envelope sender address must be given in the 
+.I SENDER
+environment variable, and the SMTP envelope recipient address in the
+.I RECIPIENT
+environment variable.
+(QMail and Postfix do this automatically.)
+.TP
+.BI \-\-skip\-prefix= string
+Before analyzing the recipient address to see which list it refers, remove 
+.I string
+from its beginning.
+This helps deal with QMail and Postfix virtual domains, see above.
+.TP
+.BI \-\-domain= domain.name
+Before analyzing the recipient address to see which list it refers, replace
+the domain name part with
+.IR domain.name .
+This helps deal with Postfix virtual domains.
+.TP
+.BI \-\-is\-list
+Does the address specified with
+.B \-\-name
+refer to a valid list?
+This sets the exit code to zero (success) if it does, or one (failure)
+if it does not.
+.TP
+.BI \-\-sendmail= pathname
+Use 
+.I pathname
+instead of
+.B /usr/sbin/sendmail
+for sending mail via a command line interface.
+Note that the command must obey the sendmail command line interface.
+.TP
+.BI \-\-smtp\-server= hostname
+Send mail using the SMTP server at
+.I hostname
+(port 25).
+The server must be configured to allow the list host to relay mail
+through it.
+Note that a command line interface is used by default;
+SMTP sending is used only if you use this option.
+.TP
+.BI \-\-qmqp\-server= hostname
+Send mail using the QMQP server at
+.I hostname
+(port 628).
+The server must be configured to allow the list host to relay mail
+through it.
+Note that a command line interface is used by default;
+QMQP sending is used only if you use this option.
+.TP
+.BI \-\-moderate
+Force an incoming message to be moderated, even if it is going to a list
+where posting is free.
+This can be used for spam filtering: 
+you pipe incoming messages through whatever spam filter you choose to use
+and if the mssage looks like spam, you ask it to be moderated by a human.
+.TP
+.BI \-\-post
+Force an incoming message to be posted, even if it is going to a list
+where posting is moderated.
+This can be used when there is an external check for whether a mail
+is acceptable to the list, e.g., by checking digital signatures.
+.TP
+.BI \-\-quiet
+By default, debugging log messages are sent to the standard error output
+stream.
+With this option, they aren't.
+.TP
+.BI \-\-sender= foo@example.com
+.TP
+.BI \-\-recipient= foo@example.com
+These two options are used with 
+.B \-\-incoming 
+and
+.B \-\-is\-list
+to override the environment variables 
+.B SENDER
+and
+.BR RECIPIENT ,
+respectively.
+.TP
+.BI \-\-get
+Get the values of one or more configuration variables.
+The name of the variables are given on the command line after the options.
+Each value is printed on a separate line.
+.TP
+.BI \-\-set 
+Set the values of one or more configuration variables.
+The names and values are given on the command line after the options
+and separated by an equals sign ("=").
+For example, the following would set the language of a list to Finnish:
+.B "enemies\-of\-carlotta \-\-name=foo@bar \-\-set language=fi"
+.TP
+.BI \-\-version
+Print out the version of the program.
+.TP
+.BI \-\-show\-lists
+List the lists enemies\-of\-carlotta knows about.
+.SH CONFIGURATION
+Each list is represented by a directory, named after the list, in 
+.IR ~/.enemies\-of\-carlotta .
+That directory contains several files and directories, described below.
+In general, it is not necessary to touch these at all.
+However, some esoteric configuration can only be done by hand editing
+of the list configuration file.
+.TP
+.B config
+The list configuration file.
+Contents are described below.
+.TP
+.B subscribers
+Subscriber database.
+Each line contains a subscriber group, with the first five space 
+delimited fields being group identifier, status, timestamp for when
+the group was created, timestamp for the bounce that made it switch
+from status 'ok' to 'bounced', and the bounce identifier.
+.TP
+.B archive\-box
+Archived messages.
+.TP
+.B bounce\-box
+Bounce messages groups not in state 'ok'.
+.TP
+.B headers\-to\-add
+These headers are added to the mails sent to the list.
+They are copied to the beginning of the existing headers exactly as they
+are in the file, after list headers ("List\-ID" and such) have been added
+and those mentioned in 
+.B headers\-to\-remove
+have been removed.
+.TP
+.B headers\-to\-remove
+These headers are removed from mails sent to the list.
+.TP
+.B moderation\-box
+Messages waiting for moderator approval.
+.TP
+.B subscription\-box
+Subscription and unsubscription requests waiting to be confirmed by the user.
+.TP
+.B templates
+Directory containing list spesific templates (optional). If this
+directory exists, templates are searched from it before going for
+system wide templates. An empty file here means the corresponding
+corresponding message is not sent at all. This can, for example, to
+be used to turn off the "please wait for moderator" mails on a per\-list
+basis.
+.TP
+.B plugins
+Directory containing plugins, Python source files that are loaded 
+automatically by EoC upon startup.
+The plugins may change how EoC operates.
+.PP
+The 
+.B config
+file has a 
+.IR keyword = value
+format:
+.PP
+.RS
+.nf
+[list]
+owners = liw@liw.iki.fi
+archived = no
+posting = free
+subscription = free
+mail\-on\-subscription\-changes = yes
+mail\-on\-forced\-unsubscribe = yes
+language = fi
+.fi
+.RE
+.PP
+The keywords 
+.BR archived , 
+.BR posting ,
+and
+.B subscription 
+correspond to the options with the same names.
+Other keywords are:
+.TP
+.B owners
+List of addresses for the owners. Set with the
+.I \-\-owner
+option.
+.TP
+.B moderators
+List of addresses for the moderators. Set with the
+.I \-\-moderator
+option.
+.TP
+.B mail\-on\-subscription\-changes
+Should the owners be mailed when users subscribe or unsubscribe?
+.TP
+.B mail\-on\-forced\-unsubscribe
+Should the owners be mailed when people are removed from the list due to
+excessive bouncing?
+.TP
+.B ignore_bounce
+Bounce messages are ignored on this list. Useful for example if
+list should have static subscriber list.
+.TP
+.B language
+Suffix for templates, to allow support for multiple languages.
+(If 
+.I language
+is set to "fi", then the template named "foo" is first searched as
+"foo.fi".)
+.TP
+.B pristine\-headers
+Do not MIME encode the headers. Set to "yes" to not encode, anything
+else (including empty or unset) means encoding will happen.
+.SH EXAMPLES
+To create a list called 
+.IR moviefans@example.com ,
+owned by
+.IR ding@example.com ,
+use the following command (all on one line):
+.sp 1
+.nf
+.RS
+enemies\-of\-carlotta \-\-name=moviefans@example.com
+\-\-owner=ding@example.com \-\-create
+.RE
+.PP
+Note that you need to arrange mail to arrive at the list (and its
+command addresses) by configuring your mail system.
+For Qmail and Postfix, see below.
+.PP
+To see the subscribers on that list:
+.sp 1
+.RS
+enemies\-of\-carlotta \-\-name=moviefans@example.com \-\-list
+.RE
+.PP
+People wanting to subscribe to the list should mail
+.sp 1
+.RS
+moviefans\-subscribe@example.com
+.RE
+.SH QMAIL
+With QMail, to arrange for incoming mail to be processed by Enemies of
+Carlotta, you need to create a couple of
+.I .qmail\-extension
+files per list.
+For example, if your username is joe and you wish to run the
+joe\-fans mailing list, you need to create two files,
+.I .qmail\-fans
+and
+.IR .qmail\-fans\-default ,
+containing
+.sp 1
+.RS
+|enemies\-of\-carlotta \-\-incoming
+.RE
+.PP
+If you're running a virtual domain, example.com, and the mails are
+being delivered to via 
+.I /var/qmail/control/virtualdomains to joe\-exampledotcom, the
+files would be called
+.I .qmail\-exampledotcom\-fans
+and
+.I .qmail\-exampledotcom\-fans\-default
+and would contain
+.sp 1
+.RS
+|enemies\-of\-carlotta \-\-incoming \-\-skip\-prefix=joe\-exampledotcom\-
+.RE
+.sp 1
+(all on one line, of course, in case the manual page formatter breaks it
+on several lines).
+.SH POSTFIX
+With Postfix, you need to set up a
+.I .forward
+file containing
+.sp 1
+.RS
+"|procmail \-p"
+.RE
+.sp 1
+and then a
+.I .procmailrc
+file containing
+.sp 1
+.RS
+:0
+.br
+* ? enemies\-of\-carlotta \-\-name=$RECIPIENT \-\-is\-list
+.br
+| enemies\-of\-carlotta \-\-incoming
+.RE
+.PP
+To use Enemies of Carlotta with a Postfix virtual domain, you need to
+set up a 
+.I "virtual regular expression map,"
+typically called
+.I /etc/postfix/virtual_regexp
+(add 
+.I "virtual_maps = regexp:/etc/postfix/virtual_regexp"
+to your 
+.I /etc/postfix/main.cf
+file to enable it).
+The regexp file needs to do ugly things to preserve the recipient
+address.
+Add the following to the regexp file:
+.sp 1
+.RS
+/^your\.virtual\.domain$/ dummy
+.br
+/^(yourlist|yourlist\-.*)@(your\.virtual\.domain)$/ joe+virtual\-$1
+.RE
+.sp 1
+That's two lines. Use
+.B joe-virtual
+instead, if
+.I recipient_delimiter
+for your Postfix is configured to a minus instead of a plus..
+Then, in your
+.I .procmailrc
+file, add the
+.I "\-\-skip\-prefix=joe\-virtual\-"
+and 
+.I \-\-domain=your.virtual.domain
+options to both calls to 
+.BR enemies\-of\-carlotta .
+.PP
+(Yes, I think these things are much too complicated, too.)
+.SH "MAIL COMMANDS"
+Users and list owners use Enemies of Carlotta via e\-mail using
+command addresses such as
+.BR foo\-subscribe@example.com .
+Here is a list of all command addresses list users and owners can give.
+In all these examples, the name of the mailing list is
+.BR foo@example.com .
+.SS "Mail commands anyone can use"
+These commands are meant for everyone's use.
+They don't require any special priviledges.
+.TP
+.BR foo@example.com
+Send mail to all list subscribers.
+The message may have to be manually approved by the list moderators first,
+and they have the power to reject a message.
+.TP
+.BR foo\-owner@example.com
+Send mail to the list owner or owners instead.
+.TP
+.BR foo\-help@example.com
+Sending mail to this address makes the list manager reply with
+the help message for the list.
+.TP
+.BR foo\-subscribe@example.com
+Send mail to this address to subscribe to a list.
+The list manager will respond with a confirmation request.
+You won't be subscribed unless you reply to the confirmation request.
+This way, malicious people can't put your address on a mailing list,
+or many mailing lists.
+.TP
+.BR foo\-subscribe\-joe=example.com@example.com
+This is a second form of the subscription address.
+If you want to subscribe to the list with another address than the
+one you're sending mail from, use this one.
+In this case, the address to be subscribed is joe@example.com.
+Note that the confirmation request is sent to Joe, since it is
+his address that is to be added to the list.
+.TP
+.BR foo\-unsubscribe@example.com
+To unsubscribe from a list, send mail to this address from the address
+that is subscribed to the list.
+Again, you will receive a confirmation request, to prevent malicious
+people from unsubscribing you against your will.
+.TP
+.BR foo\-unsubscribe\-joe=example.com@example.com
+To unsubscribe Joe, use this address.
+Again, it is Joe who gets to confirm.
+.SS "Mail commands for the list owners"
+These are commands that list owners can use to administer their list.
+.TP
+.BR foo\-subscribe\-joe=example.com@example.com
+If a list owner sends mail like this, it is they who get the confirmation
+request, not Joe.
+It is generally better for people to subscribe themselves, but sometimes
+list owners want to do it, when they have permission from the person
+and feel helpful.
+.TP
+.BR foo\-unsubscribe\-joe=example.com@example.com
+List owners can also unsubscribe other people.
+.TP
+.BR foo\-list@example.com
+To see who are on the list, this is the address to use.
+It only works if the sender address is one of the list owners.
+The sender address is the one used on the SMTP level,
+not the one in the From: header.
+.TP
+.BR foo\-setlist@example.com
+This lets a list owner set the whole subscriber list at once.
+This is similar to using lots and lots and lots of \-subscribe and
+\-unsubscribe commands, only less painful.
+Everyone who is added to the list gets a welcome message, and
+everyone who is removed from the list gets a goodbye message.
+.TP
+.BR foo\-setlistsilently@example.com
+This is similar to \-setlist, but no welcome and goodbye messages are sent.
+.SH PLUGINS
+Enemies of Carlotta supports plugins.
+If you don't know what Python programming is, you may want to skip this
+section.
+.PP
+A plugin is a Python module (file named with a 
+.B .py
+suffix), placed in the
+.B ~/.enemies\-of\-carlotta/plugins
+directory.
+The plugins are loaded automatically upon startup, if their declared
+interface version matches the one implemented by Enemies of Carlotta.
+The interface version is declared by the module global variable
+.BR PLUGIN_INTERFACE_VERSION .
+.PP
+Plugins can define hook functions that are called by appropriate places in
+the EoC code.
+At the moment, the only hook function is 
+.BR send_mail_to_subscribers_hook ,
+which can manipulate a mail message before it is sent to the subscribers.
+The function must look like this:
+.PP
+.ti +5
+def send_mail_to_subscribers_hook(list, text):
+.PP
+The
+.I list
+argument is a reference to the
+.I MailingList
+object that corresponds to the list in question, and 
+.I text
+is the complete text of the mail message as it exists.
+The function must return the new contents of the mail message.
+.SH FILES
+.TP
+.I ~/.enemies\-of\-carlotta
+All files related to your mailing lists.
+.TP
+.I ~/.enemies\-of\-carlotta/secret
+Secret password used to generate signed addresses for bounce checking
+and subscription verification.
+.TP
+.I ~/.enemies\-of\-carlotta/foo@example.com
+Directory containing data pertaining to the foo@example.com list.
+Except for the 
+.I config
+file in this directory, you shouldn't edit anything by hand.
+.TP
+.I ~/.enemies\-of\-carlotta/foo@example.com/config
+Configuration file for the mailing list.
+You may need to edit this file by hand if you wish to change moderation
+status or list owners.
+.SH "SEE ALSO"
+You may want to visit the 
+.I "Enemies of Carlotta"
+home page at
+.IR http://www.iki.fi/liw/eoc/ .
diff --git a/enemies-of-carlotta.1.es b/enemies-of-carlotta.1.es
new file mode 100644 (file)
index 0000000..40a02c1
--- /dev/null
@@ -0,0 +1,670 @@
+.TH ENEMIES-OF-CARLOTTA 1
+.SH NAME
+enemies-of-carlotta \- sencillo gestor de listas de correo
+.SH SYNOPSIS
+.B enemies-of-carlotta 
+.IR "" [ opciones "] [" direcciones ]
+.SH "DESCRIPCIÓN"
+.B enemies-of-carlotta
+es un gestor sencillo para listas de correo.
+Si no sabe qué es un gestor de listas de correo, es mejor que 
+aprenda lo que son, antes de intentar usar uno concreto.
+Por desgracia, no hay espacio para eso en una página de
+manual.
+.PP
+Enemies of Carlotta mantiene todos los datos sobre las listas de correo
+en un directorio llamado
+.I ~/.enemies-of-carlotta .
+Se creará automáticamente en cuanto Usted cree la primera lista.
+Tendrá que hacer arreglos a mano para que el gestor de listas pueda
+procesar los mensajes. 
+Los detalles varían de un servidor de correo a otro.
+Para qmail y Postfix, véase infra.
+.PP
+Cada lista tiene uno o más propietarios, que también moderan suscripciones
+o incluso algunos o todos los envíos a la lista.
+En listas sin moderación alguna, el propietario de la lista es el 
+responsable de contestar las dudas acerca de la lista.
+En listas con moderación completa, tienen que aprobar cada mensaje, antes
+de que éste pueda enviarse a la lista.
+En listas con la opción
+.IR posting=auto ,
+los mensajes de los suscriptores se envían automáticamente a la lista,
+y los moderadores tienen que aprobar el resto de mensajes.
+.SH OPCIONES
+.\" ---------------------------------------------------------------
+.TP
+.BR --name= lista@example.com
+Especifica sobre qué lista ha de actuar la orden especificada.
+Casi todas las restantes opciones precisan que especifique antes el nombre
+de la lista con la opción antedicha.
+Con las opciones
+--edit, --subscribe, --unsubscribe, y --list ,
+el nombre puede abreviarse quitando el signo @ y el dominio que le sigue.
+.\" ---------------------------------------------------------------
+.TP
+.BI --create
+Crear una lista nueva.
+Ha de especificar al menos un propietario con la opción
+.BR --owner .
+.\" ---------------------------------------------------------------
+.TP
+.BI --owner= dirección
+Al crear una lista, especifica un propietario de la lista.
+.\" ---------------------------------------------------------------
+.TP
+.BI --language= código-idioma
+Establece el código de idioma que se usa para buscar plantillas.
+El código debería estar vacío (opción por defecto, es decir inglés), o
+un código de dos letras como
+.B fi
+o
+.BR es .
+.\" ---------------------------------------------------------------
+.TP
+.B --cleaning-woman
+Se encarga de las direcciones de rebote y hace otras limpiezas varias.
+Ha de ejecutar periódicamente 
+.B "enemies-of-carlotta --cleaning-woman" ,
+algo así como una vez por hora.
+Efectuará una limpieza de todas sus listas.
+.\" ---------------------------------------------------------------
+.TP
+.BI --destroy
+Eliminar la lista.
+.\" ---------------------------------------------------------------
+.TP
+.BI --edit
+Modificar la configuración de la lista.
+.\" ---------------------------------------------------------------
+.TP
+.BI --subscription= tipo
+Al crear una lista, establece su modo de suscripción a
+.I free
+(libre) o bien
+.IR moderated 
+(moderado).
+Úselo con 
+.BR --edit ,
+o con
+.BR --create .
+.\" ---------------------------------------------------------------
+.TP
+.BI --posting= tipo
+Al crear una lista, establece su modo de envío de mensajes a
+.IR free
+(libre),
+.IR auto
+(auto),
+o bien
+.IR moderated 
+(moderado).
+Úselo con
+.BR --edit ,
+o con
+.BR --create .
+.\" ---------------------------------------------------------------
+.TP
+.BI --archived= yes-o-no
+Especifica si los mensajes de la lista deben archivarse en el directorio
+.B archive-box
+en el directorio de la lista que a su vez existe dentro del directorio
+.B "~/.enemies-of-carlotta" .
+Utilice 
+.I yes
+o bien
+.IR no .
+.\" ---------------------------------------------------------------
+.TP
+.BI --mail-on-subscription-changes= yes-o-no
+¿Debería notificarse a los dueños de la lista cuando alguien se
+suscribe o desuscribe de ella?
+Use
+.I yes
+o
+.IR no .
+Por defecto es no.
+.\" ---------------------------------------------------------------
+.TP
+.BI --mail-on-forced-unsubscription= yes-o-no
+¿Debería notificarse a los dueños de la lista cuando se elimina a
+alguien de la lista forzosamente por exceso de rebotes?
+Use
+.I yes
+o
+.IR no .
+Por defecto es no.
+.\" ---------------------------------------------------------------
+.TP
+.BI --list
+Muestra los suscriptores de una lista de correo.
+.\" ---------------------------------------------------------------
+.TP
+.BI --subscribe
+Añade suscriptores a una lista de correo.
+Los argumentos que no son opciones, son las direcciones que hay que
+suscribir a la lista.
+Observe que las direcciones que se añadan mediante este procedimiento 
+no recibirán una confirmación de suscripción, sino que se las 
+suscribirá directamente.
+.\" ---------------------------------------------------------------
+.TP
+.BI --unsubscribe
+Elimina suscriptores de una lista de correo.
+Los argumentos que no son opciones, son las direcciones que hay que
+desuscribir de la lista.
+Observe que las direcciones que se eliminen mediante este procedimiento 
+no recibirán una confirmación de desuscripción, sino que se las eliminará
+directamente.
+.\" ---------------------------------------------------------------
+.TP
+.B --incoming
+Encargarse de un mensaje que se recibe por la entrada estándar.
+La dirección del remitente del envoltorio SMTP 
+(envelope sender address) debe especificarse mediante la variable 
+de entorno 
+.I SENDER ,
+y la dirección del destinatario del envoltorio SMTP 
+(SMTP envelope recipient address) debe especificarse en la variable
+de entorno
+.I RECIPIENT .
+(qmail y Postfix lo hacen automáticamente).
+.\" ---------------------------------------------------------------
+.TP
+.BI --skip-prefix= cadena
+Antes de analizar la dirección del destinatario para ver a qué lista se
+refiere, eliminar 
+.I cadena 
+de su comienzo.
+Esta característica ayuda en el caso de los dominios virtuales de
+qmail y Postfix; véase más arriba.
+.\" ---------------------------------------------------------------
+.TP
+.BI --domain= nombre.dominio
+Antes de analizar la dirección del destinatario para ver a qué lista se 
+refiere, sustituir la parte del dominio por
+.IR nombre. dominio .
+Esta característica es útil en el caso de los dominios virtuales de
+Postfix.
+.\" ---------------------------------------------------------------
+.TP
+.BI --is-list
+¿Se refiere la lista especificada en la opción 
+.B --name
+a una lista válida?
+Devuelve un estado de salida de cero (éxito) si es válida, o un estado
+de uno (fallo) si no es válida.
+.\" ---------------------------------------------------------------
+.TP
+.BI --sendmail= ruta-hasta-el-programa
+Utilice
+.I ruta-hasta-el-programa
+en lugar de 
+.B /usr/sbin/sendmail
+para enviar correo por medio de una interfaz de línea de órdenes.
+Nótese que la orden alternativa debe seguir las convenciones de la 
+interfaz de línea de órdenes sendmail.
+.\" ---------------------------------------------------------------
+.TP
+.BI --smtp-server= nombre.de.servidor
+Enviar el correo usando el servidor SMTP 
+.I nombre.de.servidor
+(puerto 25).
+El server ha de estar configurado para permitir que la lista 
+pueda efectuar la retransmisión de correo a través de él.
+Nótese que la opción por defecto es usar la interfaz de línea
+de órdenes. Esta opción de enviar por SMTP sólo se utilizará
+si la especifica explícitamente.
+.\" ---------------------------------------------------------------
+.TP
+.BI --qmqp-server= nombredemaquina
+Enviar correo usando el servidor QMQP que hay en
+.I nombredemaquina
+(puerto 628).
+El servidor debe estar configurado para permitir que la máquina de
+la lista reenvíe correo a través suyo.
+Tenga en cuenta que por defecto se usa una interfaz de línea de
+órdenes para el envío; sólo se utilizará QMQP si especifica esta opción.
+.\" ---------------------------------------------------------------
+.TP
+.BI --moderate
+Forzar la moderación de mensajes para un mensaje dado, incluso si va a ir
+a parar a una lista de envío libre de mensajes.
+Puede usar esta opción para el filtrado de correo electrónico no 
+solicitado (spam):
+sus mensajes entrantes pasan por el filtro de spam que Usted especifique
+y si el mensaje califica como spam, se solicita la moderación del mensaje
+por parte de una persona.
+.\" ---------------------------------------------------------------
+.TP
+.BI --post
+Forzar el envío de un mensaje entrante a una lista dada, incluso si
+va a ir a parar a una lista que tenga el envío moderado.
+Puede usar esta opción cuando hay una comprobación externa de si
+un correo es aceptable en una lista; por ejempo, si dispone de 
+un comprobador de firmas digitales.
+.\" ---------------------------------------------------------------
+.TP
+.BI --quiet
+De forma predeterminada, los mensajes de registro de depuración se envían
+al flujo de salida de error estándar.
+Con esta opción, se anula dicho comportamiento.
+.\" ---------------------------------------------------------------
+.TP
+.BI --sender= foo@example.com
+.TP
+.BI --recipient= foo@example.com
+Estas dos opciones se usan junto a
+.B --incoming
+y
+.B --is-list
+para imponerse a las variables de entorno
+.B SENDER
+y
+.BR RECIPIENT ,
+respectivamente.
+.\" ---------------------------------------------------------------
+.TP
+.BI --get
+Obtiene los valores de una o más variables de configuración.
+El nombre de las variables se da en la línea de órdenes tras las opciones.
+Cada valor se imprime en una línea aparte.
+.\" ---------------------------------------------------------------
+.TP
+.BI --set
+Establece los valores de una o más variables de configuración.
+Los nombres y valores se dan en la línea de órdenes tras las opciones
+y separadas por signos 'igual' ("=").
+Por ejemplo, lo siguiente establecería el finlandés como idioma de una
+lista:
+.B "enemies-of-carlotta --name=foo@bar --set language=fi"
+.\" ---------------------------------------------------------------
+.TP
+.BI --version
+Muestra la versión del programa.
+.\" ---------------------------------------------------------------
+.SH CONFIGURACIÓN
+Cada lista está representada por un directorio, que recibe el nombre
+de la lista, y que está dentro de
+.IR ~/.enemies-of-carlotta .
+Dicho directorio contiene varios ficheros y directorios, que se describen
+más abajo. En general, no es necesario tocarlos para nada.
+Sin embargo, determinadas configuraciones, un tanto esotéricas, sólo pueden
+establecerse editando a mano el fichero de configuración de la lista.
+.TP
+.B config
+El fichero de configuración de la lista.
+Su contenido se describe más abajo.
+.TP
+.B subscribers
+Base de datos de suscriptores.
+Cada línea contiene un grupo de suscriptores, siendo los cinco 
+primeros campos delimitados por espacios el identificador del grupo,
+el estado la marca temporal de cuándo se creó el grupo, la 
+marca temporal de cuándo cambió su estado de 'ok' a 'bounced' 
+(rebotado), y el identificador de la devolución (bounce).
+.TP
+.B archive-box
+Mensajes de la lista archivados.
+.TP
+.B bounce-box
+Grupos de mensajes rebotados (bounce) y que no están en estado 'ok'.
+.TP
+.B headers-to-add
+Cabeceras a añadir a los mensajes enviados a esta lista.
+Se copian al principio de cualquier cabecera existente exactamente tal
+como estén en el fichero, tras haber añadido las cabeceras de la lista
+("List-ID", etc) y eliminado las mencionadas en
+.B headers-to-remove .
+.TP
+.B headers-to-remove
+Estas cabeceras se eliminan de los mensajes enviados a la lista.
+.TP
+.B moderation-box
+Mensajes en espera de aprobación por parte del moderador.
+.TP
+.B subscription-box
+Solicitudes de suscripción y desuscripción en espera de confirmación
+por parte del usuario.
+.TP
+.B templates
+Directorio que contiene plantillas (opcionales) específicas a la lista.
+Si existe este directorio, se buscan las plantillas allí antes de ir en
+busca de plantillas globales. Un fichero vacío indica que el mensaje
+correspondiente no será enviado. Esto puede usarse, por ejemplo, para
+desactivar los mensajes «espere por la moderación» en determinadas
+listas.
+.TP
+.B plugins
+Directorio que contiene plugins. Son archivos fuente en Python que
+carga EoC automáticamente al arrancar.
+Los plugins pueden variar la manera en que opera EoC.
+.PP
+El fichero
+.B config
+tiene un formato 
+.IR palabra_clave = valor
+:
+.PP
+.RS
+.nf
+[list]
+owners = liw@liw.iki.fi
+archived = no
+posting = free
+subscription = free
+mail-on-subscription-changes = yes
+mail-on-forced-unsubscribe = yes
+language = es
+.fi
+.RE
+.PP
+Las palabras clave
+.BR archived , 
+.BR posting ,
+y 
+.B subscription 
+corresponden a las opciones de su mismo nombre.
+Otras palabras clave son:
+.TP
+.B owners
+Lista de las direcciones de los propietarios.
+Especifíquelas con la opción
+.I --owner .
+.TP
+.B mail-on-subscription-changes
+Especifica si hay que mandar un correo a los propietarios 
+de la lista cada vez que un usuario se suscribe o se desuscribe.
+.TP
+.B mail-on-forced-unsubscribe
+Especifica si hay que mandar un correo a los propietarios de la lista 
+cada vez que un usuario es dado de baja por excesivo rebote de mensajes.
+.TP
+.B language
+Sufijo para las plantillas, para permitir el suporte de múltiples
+lenguas.
+(Si
+.I language
+tiene el valor "es", entonces a la plantilla llamada "aficionados" se la busca 
+en primer lugar como "aficionados.es".)
+.\" ---------------------------------------------------------------
+.SH EJEMPLOS
+Para crear una lista llamada
+.IR cinefilos@example.com ,
+cuyo propietario sea
+.IR dingo@example.com ,
+utilice la siguiente orden (todo en una línea):
+.sp 1
+.nf
+.RS
+enemies-of-carlotta --name=cinefilos@example.com
+--owner=dingo@example.com --create
+.RE
+.PP
+Observe que debe configurar su servidor de correo en concreto 
+para que el correo llegue a la lista.
+Para qmail y postfix, véase infra.
+.PP
+To see the subscribers on that list:
+.sp 1
+.RS
+enemies-of-carlotta --name=cinefilos@example.com --list
+.RE
+.PP
+Quien quiera suscribirse a la lista ha de escribir un correo a:
+.sp 1
+.RS
+cinefilos-subscribe@example.com
+.RE
+.SH QMAIL
+Con qmail, para conseguir que el correo entrante sea procesado por 
+Enemies of Carlotta, tiene que crear dos ficheros
+.I .qmail-extension
+por cada lista.
+Por ejemplo, si su nombre de usuario es pepe y quiere ejecutar la
+lista pepe-aficionados, ha de crear dos ficheros, 
+.I .qmail-aficionados
+y 
+.IR .qmail-aficionados-default ,
+que contengan la línea
+.sp 1
+.RS
+|enemies-of-carlotta --incoming
+.RE
+.PP
+Si tiene configurado un dominio virtual, example.com, y los correos
+se entregan vía
+.I /var/qmail/control/virtualdomains a pepe-exampledotcom ,
+los ficheros se llamarían
+.I .qmail-exampledotcom-aficionados
+y 
+.I .qmail-exampledotcom-aficionados-default
+y contendrían
+.sp 1
+.RS
+|enemies-of-carlotta --incoming --skip-prefix=pepe-exampledotcom-
+.RE
+.sp 1
+(todo en una línea, claro, por si acaso su lector de páginas de manual
+formatea la orden anterior en varias líneas).
+.SH POSTFIX
+Con postfix, ha de configurar un fichero
+.I .forward
+que contenga
+.sp 1
+.RS
+"|procmail -p"
+.RE
+.sp 1
+y además un fichero 
+.I .procmailrc
+que contenga
+.sp 1
+.RS
+:0
+.br
+* ? enemies-of-carlotta --name=$RECIPIENT --is-list
+.br
+| enemies-of-carlotta --incoming
+.RE
+.PP
+Para usar Enemies of Carlotta con un dominio virtual de Postfix, 
+ha de configurar un 
+.I "mapa virtual de expresiones regulares",
+que típicamente está en 
+.I /etc/postfix/virtual_regexp
+(añada 
+.I "virtual_maps = regexp:/etc/postfix/virtual_regexp"
+a su fichero 
+.I /etc/postfix/main.cf
+para activar esta carcterística).
+El fichero de expresiones regulares ha de hacer cosas extrañas para
+conservar la dirección del destinatario.
+Añada lo siguiente al fichero de expresiones regulares:
+.sp 1
+.RS
+/^su\.dominio\.virtual$/ dummy
+.br
+/^(sulista|sulista-.*)@(su\.dominio\.virtual)$/ pepe-virtual-$1
+.RE
+.sp 1
+(Lo anterior estaba en dos líneas).
+Luego, en su fichero
+.I .procmailrc
+añada la opción 
+.I "--skip-prefix=joe-virtual-"
+y también
+.I --domain=your.virtual.domain
+para las dos llamadas a 
+.BR enemies-of-carlotta .
+.PP
+(Sí, nosotros también pensamos que estas configuraciones son demasiado complicadas).
+.\" ---------------------------------------------------------------
+.\" ---------------------------------------------------------------
+.\" ---------------------------------------------------------------
+.SH "ÓRDENES PARA EL CORREO"
+Los usuarios y los propietarios de las listas utilizan 
+Enemies of Carlotta a través del correo electrónico, usando para ello
+direcciones a modo de órdenes, como por ejemplo
+.BR aficionados-subscribe@example.com .
+He aquí una lista de todas las órdenes que pueden usar tanto usuarios 
+como propietarios de listas de correo.
+En todos estos ejemplos, el nombre de la lista de correo es
+.BR aficionados@example.com .
+.\" ---------------------------------------------------------------
+.SS "Órdenes a través de correo que pueden usar todos"
+Estas órdenes están pensadas para el uso general.
+No precisan de ningún privilegio especial..
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados@example.com
+Enviar correo a todos los suscritos a la lista.
+El mensaje pueden haberlo aprobado previamente de forma manual los administradores
+de la lista, que están facultados para rechazar los mensajes.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-owner@example.com
+Enviar un correo al propietario o propietarios de la lista.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-help@example.com
+Enviar un correo a esta dirección hace que el gestor de listas de 
+correo nos devuelva un correo con la ayuda existente sobre la
+lista en cuestión.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-subscribe@example.com
+Envíe un mensaje a esta dirección para suscribirse a la lista.
+El gestor de listas de correo le responderá con una confirmación 
+de suscripción.
+No se le suscribirá a la lista a menos que responda a la petición
+de confirmación. 
+De esta forma, un usuario malicioso no podrá poner su dirección 
+en una o en muchas listas de correo.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-subscribe-pepe=example.com@example.com
+Esta es una manera alternativa de la dirección de suscripción.
+Si desea suscribirse a la lista de correo con una dirección distinta
+a aquella desde la que envía el mensaje, utilice esta modalidad.
+En este caso, la dirección para suscribirse es pepe@example.com.
+Nótese que la petición de confirmación se envía a Pepe, puesto
+que es su dirección la que va a añadirse a la lista.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-unsubscribe@example.com
+Para desuscribirse de una lista, envíe un correo a esta dirección
+desde la dirección que desea desuscribir de la lista.
+De nuevo recibirá una petición de confirmación, para evitar que
+un usuario malicioso le desuscriba de una lista de correo contra su 
+voluntad.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-unsubscribe-pepe=example.com@example.com
+Para desuscribir a Pepe, use esta dirección.
+De nuevo, es Pepe quien recibirá la petición de confirmación.
+.\" ---------------------------------------------------------------
+.SS "Órdenes a través de correo que pueden usar los propietarios de las listas"
+Se trata de órdenes que pueden usar los propietarios de listas para administrar su lista.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-subscribe-pepe=example.com@example.com
+Si un propietario de una lista envía un correo a la dirección anterior, 
+él recibirá la petición de confirmación, y no Pepe.
+Generalmente es mejor que los usuarios se suscriban ellos mismos, pero 
+a veces los propietarios de listas pueden desear esta característica,
+cuando tienen permiso de la persona afectada y quieren resultar más
+útiles.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-unsubscribe-joe=example.com@example.com
+Los propietarios de listas también pueden desuscribir a otros usuarios.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-list@example.com
+Para ver quién está en la lista, envíe un correo a esta dirección.
+Sólo funciona si la dirección del remitente del correo coincide
+con un propietario de la lista. La dirección "sender address" se usa
+a nivel del protocolo SMTP, y no es la del encabezamiento "From:"
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-setlist@example.com
+Esta orden permite al propietario de una lista especificar de una 
+sola vez toda la lista de suscriptores. Es equivalente a utilizar
+muchas órdenes -subscribe y -unsubscribe, sólo que menos tedioso.
+Todo el que resulte añadido a la lista recibe un mensaje de bienvenida,
+y todo el que quede eliminado de la lista recibe un mensaje de despedida.
+.\" ---------------------------------------------------------------
+.TP
+.BR aficionados-setlistsilently@example.com
+Semejante a -setlist, pero no se envían mensajes ni de bienvenida, ni 
+de despedida.
+.\" ---------------------------------------------------------------
+.\" ---------------------------------------------------------------
+.\" ---------------------------------------------------------------
+.SH PLUGINS
+Enemies of Carlotta admite plugins.
+Si no sabe programar en Python, probablemente se puede saltar esta
+sección.
+.PP
+Un plugin es un módulo de Python (fichero con un sufijo
+.B .py
+en el nombre), situado en el directorio
+.B ~/.enemies-of-carlotta/plugins .
+Los plugins se cargan automáticamente durante el arranque, si la versión
+declarada de su interfaz se ajusta con la implementada por Enemies of
+Carlotta. La versión de la interfaz se declara en la variable global del
+módulo
+.BR PLUGIN_INTERFACE_VERSION .
+.PP
+Los plugin pueden definir funciones que serán invocadas desde los lugares
+apropiados del código EoC.
+Por el momento, la única función de enganche (hook) disponible es
+.BR send_mail_to_subscribers_hook ,
+que puede manipular un mensaje antes de que sea enviado a los suscriptores.
+La función debe parecerse a esto:
+.PP
+.ti +5
+def send_mail_to_subscribers_hook(list, text):
+.PP
+El argumento
+.I list
+es una referencia al objeto
+.I MailingList
+que corresponde a la lista en cuestión, y
+.I text
+es el texto completo del mensaje de correo en su forma actual.
+La función debe devolver el nuevo contenido del mensaje de correo.
+.\" ---------------------------------------------------------------
+.\" ---------------------------------------------------------------
+.\" ---------------------------------------------------------------
+.SH FICHEROS
+.TP
+.I ~/.enemies-of-carlotta
+Aquí están todos los ficheros relacionados con sus listas de correo.
+.TP
+.I ~/.enemies-of-carlotta/secret
+Contraseña secreta que se usa para generar direcciones firmadas
+para comprobación de rebotes de correo y verificación de suscripción.
+.TP
+.I ~/.enemies-of-carlotta/aficionados@example.com
+Directorio que contiene los datos relativos a la lista 
+aficionados@example.com. Excepto el fichero
+.I config
+de este directorio, no debe editar a mano nada de lo contenido en él.
+.TP
+.I ~/.enemies-of-carlotta/aficionados@example.com/config
+Fichero de configuración de la lista de correo.
+Quizá tenga que editar este fichero a mano si desea cambiar el estado
+de moderación de la lista o sus propietarios.
+.SH "VÉASE TAMBIÉN"
+Visite la página de 
+.I "Enemies of Carlotta"
+alojada en 
+.IR http://www.iki.fi/liw/eoc/ .
+.TP
+La traducción de esta página ha corrido a cargo de Iván Juanes
+.BR kerberos@gulic.org
+y de Ricardo Cárdenes
+.BR heimy@gulic.org
+como parte de los proyectos del grupo Gulic.
diff --git a/enemies-of-carlotta.1.fr b/enemies-of-carlotta.1.fr
new file mode 100644 (file)
index 0000000..4797929
--- /dev/null
@@ -0,0 +1,421 @@
+.\" Relecture Gérard Delafond
+.TH ENEMIES-OF-CARLOTTA 1
+.SH NOM
+enemies-of-carlotta \- un gestionnaire de listes de diffusion simple
+.SH SYNOPSIS
+.B enemies-of-carlotta 
+.IR "" [ options "] [" adresses ]
+.SH "DESCRIPTION"
+.B enemies-of-carlotta
+est un gestionnaire de listes de diffusion simple.
+Si vous ne savez pas ce qu'est un gestionnaire de listes de diffusion, vous
+devez commencez par apprendre de quoi il s'agit avant de vouloir vous en 
+servir.
+.PP
+Enemies of Carlotta conserve toutes les données des listes dans le répertoire
+.I ~/.enemies-of-carlotta.
+Il sera créé automatiquement lorsque vous créerez votre première liste.
+Vous devez vous arranger manuellement pour que les courriels soient traités
+par le gestionnaire de liste de diffusion.
+Les détails peuvent varier d'un système à l'autre.
+Pour QMail et Postfix, regardez ci-dessous.
+.PP
+Chaque liste possède un ou plusieurs propriétaires, qui modèrent également
+les inscriptions, ou bien certains messages, voire tous.
+Sur les listes non modérées, les propriétaires de la liste sont chargés de
+répondre aux questions au sujet de la liste.
+Sur les liste totalement modérées, ils doivent approuver chaque message
+avant qu'il ne soit envoyé sur la liste.
+Sur les liste où
+.IR posting=auto ,
+les messages des abonnés sont automatiquement envoyés à la liste, et les
+modérateurs n'ont pas besoin d'approuver que les messages qui ne 
+proviennent pas des abonnés.
+.SH OPTIONS
+.\" ---------------------------------------------------------------
+.TP
+.BR --name= foo@example.com
+Précise la liste sur laquelle la commande doit être effectuée.
+La plupart des options restantes nécessitent que vous précisiez
+le nom de la liste avec cette option.
+Avec les options --edit, --subscribe, --unsubscribe, et --list,
+le nom peut être abrégé en enlevant le signe @ et le domaine.
+.\" ---------------------------------------------------------------
+.TP
+.BI --create
+Créer une nouvelle liste de diffusion
+Vous devez précisez au moins un propriétaire à l'aide de 
+.BR --owner .
+.\" ---------------------------------------------------------------
+.TP
+.BI --owner= adresse
+Indique la liste des propriétaires lorsque la liste est créée.
+.\" ---------------------------------------------------------------
+.TP
+.B --cleaning-woman
+Règle les problèmes des messages qui n'ont pas pu être délivrés et
+nettoie également quelques autres éléments.
+Vous devez lancer
+.B "enemies-of-carlotta --cleaning-woman"
+périodiquement.
+Cela nettoiera l'ensemble de vos listes de diffusion.
+.\" ---------------------------------------------------------------
+.TP
+.BI --destroy
+Détruit la liste.
+.\" ---------------------------------------------------------------
+.TP
+.BI --edit
+Modifie la configuration de la liste.
+.\" ---------------------------------------------------------------
+.TP
+.BI --subscription= type
+Lors de la création d'une liste, définit le mode d'abonnement à 
+.I free
+(libre) ou 
+.IR moderated
+(modéré).
+Utilisez-le avec
+.BR --edit ,
+ou
+.BR --create .
+.\" ---------------------------------------------------------------
+.TP
+.BI --posting= type
+Lors de la création d'une liste, définit le mode d'envoi à 
+.IR free ,
+(libre)
+.IR auto ,
+(automatique)
+ou
+.IR moderated
+(modéré).
+Utilisez-le avec
+.BR --edit ,
+ou
+.BR --create .
+.\" ---------------------------------------------------------------
+.TP
+.BI --archived= yes-ou-no
+Les messages à destination de la liste doivent-ils être archivés sous
+la forme d'une
+.B archive-box
+ou d'un répertoire dans le répertoire dans liste 
+.B "~/.enemies-of-carlotta"
+.
+Utilisez 
+.I yes
+(oui) ou 
+.IR no
+(non).
+.\" ---------------------------------------------------------------
+.TP
+.BI --list
+Liste l'ensemble des abonnés à une liste de diffusion.
+.\" ---------------------------------------------------------------
+.TP
+.BI --subscribe
+Ajoute des abonnés à la liste.
+Les arguments qui ne sont pas des options sont les adresses qui doivent
+être abonnées. Notez que les adresses ajoutées de cette manière ne 
+recevront pas de demande de confirmation d'abonnement.
+.\" ---------------------------------------------------------------
+.TP
+.BI --unsubscribe
+Enlève un abonné de la liste.
+Les arguments qui ne sont pas des options sont les adresses qui doivent
+être désabonnées. Notez que les adresses enlevées de cette manière ne
+recevront pas de demande de confirmation de désabonnement.
+.\" ---------------------------------------------------------------
+.TP
+.B --incoming
+Traite un message entrant sur l'entrée standard.
+L'enveloppe SMTP qui précise l'adresse de l'expéditeur doit être
+précisée par la variable d'environnement
+.I SENDER
+et l'enveloppe SMTP qui précise l'adresse du destinataire doit être
+précisée par la variable d'environnement
+.I RECIPIENT
+(QMail et Postfix traitent cela automatiquement.)
+.\" ---------------------------------------------------------------
+.TP
+.BI --skip-prefix= chaîne
+Avant d'analyser l'adresse de destination pour regarder à quelle liste
+le message est destiné, ce paramètre permet d'enlever
+.I chaîne
+depuis son début.
+C'est utilisé pour traiter les domaines virtuels dans QMail et Postfix,
+voyez ci-dessous.
+.\" ---------------------------------------------------------------
+.TP
+.BI --domain= nom.domaine
+Avant d'analyser l'adresse de destination pour voir à quelle liste le message
+est destiné, remplace le nom de domaine par
+.IR nom.domaine .
+C'est utilisé pour traiter les domaines virtuels dans QMail et Postfix.
+.\" ---------------------------------------------------------------
+.TP
+.BI --is-list
+L'adresse précisée par
+.B --name
+fait-elle référence à un nom de liste valide\ ?
+Cela retourne le code d'erreur zéro (succès) si c'est le cas, ou un (échec)
+dans le cas contraire.
+.\" ---------------------------------------------------------------
+.TP
+.BI --sendmail= chemin
+Utilise
+.I chemin
+au lieu de 
+.B /usr/sbin/sendmail
+Pour envoyer des courriels via une interface en ligne de commande.
+Notez que la commande doit respecter l'interface de la ligne de commande
+sendmail.
+.\" ---------------------------------------------------------------
+.TP
+.BI --smtp-server= hôte
+Envoi les courriels en utilisant le serveur 
+.I hôte
+(port 25).
+Le serveur doit être configuré pour permettre à la machine sur laquelle
+fonctionne la liste de l'utiliser pour relais.
+Notez que par défaut, c'est l'interface en ligne de commande qui est utilisée.
+L'envoi au moyen de SMTP n'est utilisé que si vous utilisez cette option.
+.\" ---------------------------------------------------------------
+.TP
+.BI --moderate
+Force un message entrant à être modéré, même s'il est envoyé à une liste
+où l'envoi est libre.
+Cela peut être utilisé pour filtrer le spam\ :
+Vos messages entrants peuvent être filtrés par n'importe quel système de 
+filtrage des courriels, dès lors qu'un message semble être du spam,
+vous pouvez demander qu'il soit modéré par une personne humaine.
+.\" ---------------------------------------------------------------
+.TP
+.BI --quiet
+Par défaut, les messages de débogage des journaux sont envoyés sur 
+l'erreur standard.
+Avec cette option, il ne le sont plus.
+.\" ---------------------------------------------------------------
+.SH CONFIGURATION
+Chaque liste est représentée par un répertoire, nommé d'après le nom de
+la liste, sous le répertoire 
+.IR ~/.enemies-of-carlotta .
+Ce répertoire contient plusieurs fichiers et répertoires, qui sont décrits
+ci-dessous.
+En général, il n'est pas nécessaire de toucher à ces répertoires.
+Cependant, certaines configurations ésotériques peuvent uniquement être
+faites en éditant le fichier de configuration de la liste.
+.TP
+.B config
+Le fichier de configuration de la liste.
+Le contenu est décrit ci-dessous.
+.TP
+.B subscribers
+La base de données des abonnés.
+Chaque ligne contient un groupe d'abonné, dont les cinq premiers
+espaces délimitent les champs qui sont les identifiants des groupes,
+le statut, le timestamp de la date de création du groupe, le timestamp
+pour les retours de courriel en cas d'échec, il peut varier de «\ ok\ »
+à «\ bonced\ », et l'identifiant du message de retour en cas d'échec.
+.TP
+.B archive-box
+Les messages archivés.
+.TP
+.B bounce-box
+Groupes de messages qui n'ont pu être délivrés et qui sont dans le
+statut «\ ok\ ».
+.TP
+.B moderation-box
+Messages en attente d'approbation du modérateur.
+.TP
+.B subscription-box
+Requêtes d'abonnement de désabonnement en attente de confirmation de la
+part de l'utilisateur.
+.PP
+le fichier 
+.B config
+possède des
+.IR mot-clé = value
+format\ :
+.PP
+.RS
+.nf
+[list]
+owners = liw@liw.iki.fi
+archived = no
+posting = free
+subscription = free
+mail-on-subscription-changes = yes
+mail-on-forced-unsubscribe = yes
+language = fi
+.fi
+.RE
+.PP
+Les mots clés
+.BR archived , 
+.BR posting ,
+et
+.B subscription 
+correspondent aux options qui portent les mêmes noms.
+Les autres mots-clés sont\ :
+.TP
+.B owners
+Liste les adresses des propriétaires. Définissez-la à l'aide de
+l'option
+.I --owner.
+.TP
+.B mail-on-subscription-changes
+Les propriétaires doivent-ils recevoir un avertissement lorsqu'un utilisateur
+s'abonne ou se désabonne\ ?
+.TP
+.B mail-on-forced-unsubscribe
+Les propriétaires doivent-ils recevoir un avertissement lorsqu'une personne
+est enlevée de la liste car trop de courriels ne lui sont pas parvenus\ ?
+.TP
+.B language
+Suffixe pour les templates, pour permettre le support de plusieurs
+langues
+(Si 
+.I language
+est défini à «\ fr\ », alors la template nommée «\ foo\ » est d'abord
+recherchée comme «\ foo.fr\ ».)
+.\" ---------------------------------------------------------------
+.SH EXEMPLES
+Pour créer une liste nommée
+.IR moviefans@example.com ,
+dont le propriétaire est
+.IR ding@example.com ,
+utilisez la commande suivante (tout sur la même ligne)\ :
+.sp 1
+.RS
+enemies-of-carlotta --name=ding@example.com --create
+.RE
+.PP
+Pour voir la liste de tous les abonnés à cette liste\ :
+.sp 1
+.RS
+enemies-of-carlotta --name=moviefans@example.com --list
+.RE
+.PP
+Les personnes qui souhaitent être abonnées à la liste doivent
+envoyer un courriel à
+.sp 1
+.RS
+moviefans-subscribe@example.com
+.RE
+.SH QMAIL
+Dans Qmail, pour faire en sorte que le courrier entrant soit traité
+par Enemies of Carlotta, vous avez besoin de créer quelques fichiers
+.I .qmail-extension
+par liste.
+Par exemple, si votre nom d'utilisateur est joe et que vous souhaitez
+utiliser la liste de diffusion joe-fans, vous devrez créer les
+fichiers
+.I .qmail-fans
+et
+.IR .qmail-fans-default ,
+qui contiennent
+.sp 1
+.RS
+|"enemies-of-carlotta --incoming"
+.RE
+.PP
+Si vous utilisez un domaine virtuel, example.com, et que les 
+courriels sont délivrés via
+.I /var/qmail/control/virtualdomains à joe-exampledotcom, 
+les fichiers seront nommés
+.I .qmail-exampledotcom-fans
+et
+.I .qmail-exampledotcom-fans-default
+et ils contiendront
+.sp 1
+.RS
+|"enemies-of-carlotta --incoming 
+.br
+--skip-prefix=joe-exampledotcom-"
+.RE
+.sp 1
+(l'ensemble sur la même ligne, il va de soi).
+.SH POSTFIX
+Avec Postfix, vous devrez configurer un fichier
+.I .forward
+contenant
+.sp 1
+.RS
+"|procmail -p"
+.RE
+.sp 1
+et un fichier
+.I .procmailrc
+contenant
+.sp 1
+.RS
+:0
+.br
+* ? enemies-of-carlotta --name=$RECIPIENT --is-list
+.br
+| enemies-of-carlotta --incoming
+.RE
+.PP
+Pour utiliser Enemies of Carlotta avec un domaine virtuel Postfix,
+vous devrez mettre en place une
+.I «\ carte virtuelle d'expressions rationnelles\ »,
+généralement 
+.I /etc/postfix/virtual_regexp
+(ajoutez
+.I "virtual_maps = regexp:/etc/postfix/virtual_regexp"
+dans votre fichier
+.I /etc/postfix/main.cf
+pour l'activer).
+Le fichier d'expressions rationnelles a besoin de faire des choses
+horribles pour conserver l'adresse de destination.
+Ajoutez ce qui suit dans le fichier d'expressions rationnelles\ :
+.sp 1
+.RS
+/^your\.virtual\.domain$/ dummy
+.br
+/^(yourlist|yourlist-.*)@(your\.virtual\.domain)$/ joe-virtual-$1
+.RE
+.sp 1
+(Ça fait deux lignes.)
+Ensuite dans votre fichier,
+.I .procmailrc, 
+ajoutez les options
+.I "--skip-prefix=joe-virtual-"
+et
+.I --domain=your.virtual.domain
+pour les deux appels à 
+.BR enemies-of-carlotta .
+.PP
+(Oui, je aussi trouve que ces choses là sont trop compliquées)
+.SH FICHIERS
+.TP
+.I ~/.enemies-of-carlotta
+L'ensemble des fichiers en rapports avec vos listes de diffusion.
+.TP
+.I ~/.enemies-of-carlotta/secret
+Les mots de passe secrets utilisés pour générer des adresses signées pour
+le contrôle des couriels d'échec et la validation de l'abonnement.
+.TP
+.I ~/.enemies-of-carlotta/foo@example.com
+Le répertoire contient les données appartenant à la liste de diffusion
+foo@example.com.
+À l'exception du fichier de 
+.I config
+qui se trouve dans ce répertoire, vous ne devriez pas éditer autre chose 
+à la main
+.TP
+.I ~/.enemies-of-carlotta/foo@example.com/config
+Fichier de configuration pour la liste de diffusion.
+Vous aurez peut-être besoin de l'éditer à la main si vous souhaitez 
+apporter des changement en ce qui concerne la modération ou bien les 
+propriétaires de la liste.
+.SH "VOIR AUSSI"
+Vous serez peut-être intéressé de visiter 
+la page d'accueil d'
+.I «\ Enemies of Carlotta\ »
+à l'adresse
+.IR http://www.iki.fi/liw/eoc/ .
+.SH "TRADUCTION"
+Pierre Machard <pmachard@tuxfamily.org>, 2003
diff --git a/eoc.py b/eoc.py
new file mode 100644 (file)
index 0000000..b47b147
--- /dev/null
+++ b/eoc.py
@@ -0,0 +1,1675 @@
+"""Mailing list manager.
+
+This is a simple mailing list manager that mimicks the ezmlm-idx mail
+address commands. See manual page for more information.
+"""
+
+VERSION = "1.1.5"
+PLUGIN_INTERFACE_VERSION = "1"
+
+import getopt
+import md5
+import os
+import shutil
+import smtplib
+import string
+import sys
+import time
+import ConfigParser
+try:
+    import email.Header
+    have_email_module = 1
+except ImportError:
+    have_email_module = 0
+import imp
+
+import qmqp
+
+
+# The following values will be overriden by "make install".
+TEMPLATE_DIRS = ["./templates"]
+DOTDIR = "dot-eoc"
+
+
+class EocException(Exception):
+
+    def __init__(self, arg=None):
+        self.msg = repr(arg)
+
+    def __str__(self):
+        return self.msg
+
+class UnknownList(EocException):
+    def __init__(self, list_name):
+        self.msg = "%s is not a known mailing list" % list_name
+
+class BadCommandAddress(EocException):
+    def __init__(self, address):
+        self.msg = "%s is not a valid command address" % address
+
+class BadSignature(EocException):
+    def __init__(self, address):
+        self.msg = "address %s has an invalid digital signature" % address
+
+class ListExists(EocException):
+    def __init__(self, list_name):
+        self.msg = "Mailing list %s alreadys exists" % list_name
+
+class ListDoesNotExist(EocException):
+    def __init__(self, list_name):
+        self.msg = "Mailing list %s does not exist" % list_name
+
+class MissingEnvironmentVariable(EocException):
+    def __init__(self, name):
+        self.msg = "Environment variable %s does not exist" % name
+
+class MissingTemplate(EocException):
+    def __init__(self, template):
+        self.msg = "Template %s does not exit" % template
+
+
+# Names of commands EoC recognizes in e-mail addresses.
+SIMPLE_COMMANDS = ["help", "list", "owner", "setlist", "setlistsilently", "ignore"]
+SUB_COMMANDS = ["subscribe", "unsubscribe"]
+HASH_COMMANDS = ["subyes", "subapprove", "subreject", "unsubyes",
+                 "bounce", "probe", "approve", "reject", "setlistyes",
+                 "setlistsilentyes"]
+COMMANDS = SIMPLE_COMMANDS + SUB_COMMANDS + HASH_COMMANDS
+
+
+def md5sum_as_hex(s):
+    return md5.new(s).hexdigest()
+
+environ = None
+
+def set_environ(new_environ):
+    global environ
+    environ = new_environ
+
+def get_from_environ(key):
+    global environ
+    if environ:
+        env = environ
+    else:
+        env = os.environ
+    if env.has_key(key):
+        return env[key].lower()
+    raise MissingEnvironmentVariable(key)
+
+class AddressParser:
+
+    """A parser for incoming e-mail addresses."""
+
+    def __init__(self, lists):
+        self.set_lists(lists)
+        self.set_skip_prefix(None)
+        self.set_forced_domain(None)
+
+    def set_lists(self, lists):
+        """Set the list of canonical list names we should know about."""
+        self.lists = lists
+
+    def set_skip_prefix(self, skip_prefix):
+        """Set the prefix to be removed from an address."""
+        self.skip_prefix = skip_prefix
+        
+    def set_forced_domain(self, forced_domain):
+        """Set the domain part we should force the address to have."""
+        self.forced_domain = forced_domain
+
+    def clean(self, address):
+        """Remove cruft from the address and convert the rest to lower case."""
+        if self.skip_prefix:
+            n = self.skip_prefix and len(self.skip_prefix)
+            if address[:n] == self.skip_prefix:
+                address = address[n:]
+        if self.forced_domain:
+            parts = address.split("@", 1)
+            address = "%s@%s" % (parts[0], self.forced_domain)
+        return address.lower()
+
+    def split_address(self, address):
+        """Split an address to a local part and a domain."""
+        parts = address.lower().split("@", 1)
+        if len(parts) != 2:
+            return (address, "")
+        else:
+            return parts
+
+    # Does an address refer to a list? If not, return None, else return a list
+    # of additional parts (separated by hyphens) in the address. Note that []
+    # is not the same as None.
+    
+    def additional_address_parts(self, address, listname):
+        addr_local, addr_domain = self.split_address(address)
+        list_local, list_domain = self.split_address(listname)
+        
+        if addr_domain != list_domain:
+            return None
+        
+        if addr_local.lower() == list_local.lower():
+            return []
+        
+        n = len(list_local)
+        if addr_local[:n] != list_local or addr_local[n] != "-":
+            return None
+            
+        return addr_local[n+1:].split("-")
+        
+
+    # Parse an address we have received that identifies a list we manage.
+    # The address may contain command and signature parts. Return the name
+    # of the list, and a sequence of the additional parts (split at hyphens).
+    # Raise exceptions for errors. Note that the command will be valid, but
+    # cryptographic signatures in the address is not checked.
+    
+    def parse(self, address):
+        address = self.clean(address)
+        for listname in self.lists:
+            parts = self.additional_address_parts(address, listname)
+            if parts == None:
+                pass
+            elif parts == []:
+                return listname, parts
+            elif parts[0] in HASH_COMMANDS:
+                if len(parts) != 3:
+                    raise BadCommandAddress(address)
+                return listname, parts
+            elif parts[0] in COMMANDS:
+                return listname, parts
+
+        raise UnknownList(address)
+
+
+class MailingListManager:
+
+    def __init__(self, dotdir, sendmail="/usr/sbin/sendmail", lists=[],
+                 smtp_server=None, qmqp_server=None):
+        self.dotdir = dotdir
+        self.sendmail = sendmail
+        self.smtp_server = smtp_server
+        self.qmqp_server = qmqp_server
+
+        self.make_dotdir()
+        self.secret = self.make_and_read_secret()
+
+        if not lists:
+            lists = filter(lambda s: "@" in s, os.listdir(dotdir))
+        self.set_lists(lists)
+
+        self.simple_commands = ["help", "list", "owner", "setlist",
+                                "setlistsilently", "ignore"]
+        self.sub_commands = ["subscribe", "unsubscribe"]
+        self.hash_commands = ["subyes", "subapprove", "subreject", "unsubyes",
+                              "bounce", "probe", "approve", "reject",
+                              "setlistyes", "setlistsilentyes"]
+        self.commands = self.simple_commands + self.sub_commands + \
+                        self.hash_commands
+
+        self.environ = None
+
+        self.load_plugins()
+        
+    # Create the dot directory for us, if it doesn't exist already.
+    def make_dotdir(self):
+        if not os.path.isdir(self.dotdir):
+            os.makedirs(self.dotdir, 0700)
+
+    # Create the "secret" file, with a random value used as cookie for
+    # verification addresses.
+    def make_and_read_secret(self):
+        secret_name = os.path.join(self.dotdir, "secret")
+        if not os.path.isfile(secret_name):
+            f = open("/dev/urandom", "r")
+            secret = f.read(32)
+            f.close()
+            f = open(secret_name, "w")
+            f.write(secret)
+            f.close()
+        else:
+            f = open(secret_name, "r")
+            secret = f.read()
+            f.close()
+        return secret
+
+    # Load the plugins from DOTDIR/plugins/*.py.
+    def load_plugins(self):
+        self.plugins = []
+
+        dirname = os.path.join(DOTDIR, "plugins")
+        try:
+            plugins = os.listdir(dirname)
+        except OSError:
+            return
+            
+        plugins.sort()
+        plugins = map(os.path.splitext, plugins)
+        plugins = filter(lambda p: p[1] == ".py", plugins)
+        plugins = map(lambda p: p[0], plugins)
+        for name in plugins:
+            pathname = os.path.join(dirname, name + ".py")
+            f = open(pathname, "r")
+            module = imp.load_module(name, f, pathname, 
+                                     (".py", "r", imp.PY_SOURCE))
+            f.close()
+            if module.PLUGIN_INTERFACE_VERSION == PLUGIN_INTERFACE_VERSION:
+                self.plugins.append(module)
+
+    # Call function named funcname (a string) in all plugins, giving as
+    # arguments all the remaining arguments preceded by ml. Return value
+    # of each function is the new list of arguments to the next function.
+    # Return value of this function is the return value of the last function.
+    def call_plugins(self, funcname, list, *args):
+        for plugin in self.plugins:
+            if plugin.__dict__.has_key(funcname):
+                args = apply(plugin.__dict__[funcname], (list,) + args)
+                if type(args) != type((0,)):
+                    args = (args,)
+        return args
+
+    # Set the list of listnames. The list of lists needs to be sorted in
+    # length order so that test@example.com is matched before
+    # test-list@example.com
+    def set_lists(self, lists):
+        temp = map(lambda s: (len(s), s), lists)
+        temp.sort()
+        self.lists = map(lambda t: t[1], temp)
+
+    # Return the list of listnames.
+    def get_lists(self):
+        return self.lists
+
+    # Decode an address that has been encoded to be part of a local part.
+    def decode_address(self, parts):
+        return string.join(string.join(parts, "-").split("="), "@")
+
+    # Is local_part@domain an existing list?
+    def is_list_name(self, local_part, domain):
+        return ("%s@%s" % (local_part, domain)) in self.lists
+
+    # Compute the verification checksum for an address.
+    def compute_hash(self, address):
+        return md5sum_as_hex(address + self.secret)
+
+    # Is the verification signature in a parsed address bad? If so, return true,
+    # otherwise return false.
+    def signature_is_bad(self, dict, hash):
+        local_part, domain = dict["name"].split("@")
+        address = "%s-%s-%s@%s" % (local_part, dict["command"], dict["id"], 
+                                   domain)
+        correct = self.compute_hash(address)
+        return correct != hash
+
+    # Parse a command address we have received and check its validity
+    # (including signature, if any). Return a dictionary with keys
+    # "command", "sender" (address that was encoded into address, if
+    # any), "id" (group ID).
+
+    def parse_recipient_address(self, address, skip_prefix, forced_domain):
+        ap = AddressParser(self.get_lists())
+        ap.set_lists(self.get_lists())
+        ap.set_skip_prefix(skip_prefix)
+        ap.set_forced_domain(forced_domain)
+        listname, parts = ap.parse(address)
+
+        dict = { "name": listname }
+
+        if parts == []:
+            dict["command"] = "post"
+        else:
+            command, args = parts[0], parts[1:]
+            dict["command"] = command
+            if command in SUB_COMMANDS:
+                dict["sender"] = self.decode_address(args)
+            elif command in HASH_COMMANDS:
+                dict["id"] = args[0]
+                hash = args[1]
+                if self.signature_is_bad(dict, hash):
+                    raise BadSignature(address)
+
+        return dict
+
+    # Does an address refer to a mailing list?
+    def is_list(self, name, skip_prefix=None, domain=None):
+        try:
+            self.parse_recipient_address(name, skip_prefix, domain)
+        except BadCommandAddress:
+            return 0
+        except BadSignature:
+            return 0
+        except UnknownList:
+            return 0
+        return 1
+
+    # Create a new list and return it.
+    def create_list(self, name):
+        if self.is_list(name):
+            raise ListExists(name)
+        self.set_lists(self.lists + [name])
+        return MailingList(self, name)
+
+    # Open an existing list.
+    def open_list(self, name):
+        if self.is_list(name):
+            return self.open_list_exact(name)
+        else:
+            x = name + "@"
+            for list in self.lists:
+                if list[:len(x)] == x:
+                    return self.open_list_exact(list)
+            raise ListDoesNotExist(name)
+
+    def open_list_exact(self, name):
+        for list in self.get_lists():
+            if list.lower() == name.lower():
+                return MailingList(self, list)
+        raise ListDoesNotExist(name)
+
+    # Process an incoming message.
+    def incoming_message(self, skip_prefix, domain, moderate, post):
+        debug("Processing incoming message.")
+        debug("$SENDER = <%s>" % get_from_environ("SENDER"))
+        debug("$RECIPIENT = <%s>" % get_from_environ("RECIPIENT"))
+        dict = self.parse_recipient_address(get_from_environ("RECIPIENT"),
+                                                             skip_prefix, 
+                                                             domain)
+        dict["force-moderation"] = moderate
+        dict["force-posting"] = post
+        debug("List is <%(name)s>, command is <%(command)s>." % dict)
+        list = self.open_list_exact(dict["name"])
+        list.obey(dict)
+
+    # Clean up bouncing address and do other janitorial work for all lists.
+    def cleaning_woman(self, send_mail=None):
+        now = time.time()
+        for listname in self.lists:
+            list = self.open_list_exact(listname)
+            if send_mail:
+                list.send_mail = send_mail
+            list.cleaning_woman(now)
+
+    # Send a mail to the desired recipients.
+    def send_mail(self, envelope_sender, recipients, text):
+        debug("send_mail:\n  sender=%s\n  recipients=%s\n  text=\n    %s" % 
+              (envelope_sender, str(recipients), 
+               "\n    ".join(text[:text.find("\n\n")].split("\n"))))
+        if recipients:
+            if self.smtp_server:
+                smtp = smtplib.SMTP(self.smtp_server)
+                smtp.sendmail(envelope_sender, recipients, text)
+                smtp.quit()
+            elif self.qmqp_server:
+                q = qmqp.QMQP(self.qmqp_server)
+                q.sendmail(envelope_sender, recipients, text)
+                q.quit()
+            else:
+                recipients = string.join(recipients, " ")
+                f = os.popen("%s -oi -f '%s' %s" % 
+                                 (self.sendmail, 
+                                  envelope_sender, 
+                                  recipients),
+                             "w")
+                f.write(text)
+                f.close()
+        else:
+            debug("send_mail: no recipients, not sending")
+
+
+
+class MailingList:
+
+    posting_opts = ["auto", "free", "moderated"]
+
+    def __init__(self, mlm, name):
+        self.mlm = mlm
+        self.name = name
+
+        self.cp = ConfigParser.ConfigParser()
+        self.cp.add_section("list")
+        self.cp.set("list", "owners", "")
+        self.cp.set("list", "moderators", "")
+        self.cp.set("list", "subscription", "free")
+        self.cp.set("list", "posting", "free")
+        self.cp.set("list", "archived", "no")
+        self.cp.set("list", "mail-on-subscription-changes", "no")
+        self.cp.set("list", "mail-on-forced-unsubscribe", "no")
+        self.cp.set("list", "ignore-bounce", "no")
+        self.cp.set("list", "language", "")
+        self.cp.set("list", "pristine-headers", "")
+
+        self.dirname = os.path.join(self.mlm.dotdir, name)
+        self.make_listdir()
+        self.cp.read(self.mkname("config"))
+
+        self.subscribers = SubscriberDatabase(self.dirname, "subscribers")
+        self.moderation_box = MessageBox(self.dirname, "moderation-box")
+        self.subscription_box = MessageBox(self.dirname, "subscription-box")
+        self.bounce_box = MessageBox(self.dirname, "bounce-box")
+
+    def make_listdir(self):
+        if not os.path.isdir(self.dirname):
+            os.mkdir(self.dirname, 0700)
+            self.save_config()
+            f = open(self.mkname("subscribers"), "w")
+            f.close()
+
+    def mkname(self, relative):
+        return os.path.join(self.dirname, relative)
+
+    def save_config(self):
+        f = open(self.mkname("config"), "w")
+        self.cp.write(f)
+        f.close()
+
+    def read_stdin(self):
+        data = sys.stdin.read()
+        # Skip Unix mbox "From " mail start indicator
+        if data[:5] == "From ":
+            data = string.split(data, "\n", 1)[1]
+        return data
+
+    def invent_boundary(self):
+        return "%s/%s" % (md5sum_as_hex(str(time.time())),
+                          md5sum_as_hex(self.name))
+
+    def command_address(self, command):
+        local_part, domain = self.name.split("@")
+        return "%s-%s@%s" % (local_part, command, domain)
+
+    def signed_address(self, command, id):
+        unsigned = self.command_address("%s-%s" % (command, id))
+        hash = self.mlm.compute_hash(unsigned)
+        return self.command_address("%s-%s-%s" % (command, id, hash))
+
+    def ignore(self):
+        return self.command_address("ignore")
+
+    def nice_7bit(self, str):
+        for c in str:
+            if (ord(c) < 32 and not c.isspace()) or ord(c) >= 127:
+                return False
+        return True
+    
+    def mime_encode_headers(self, text):
+        headers, body = text.split("\n\n", 1)
+    
+        list = []
+        for line in headers.split("\n"):
+            if line[0].isspace():
+                list[-1] += line
+            else:
+                list.append(line)
+    
+        headers = []
+        for header in list:
+            if self.nice_7bit(header):
+                headers.append(header)
+            else:
+                if ": " in header:
+                    name, content = header.split(": ", 1)
+                else:
+                    name, content = header.split(":", 1)
+                hdr = email.Header.Header(content, "utf-8")
+                headers.append(name + ": " + hdr.encode())
+    
+        return "\n".join(headers) + "\n\n" + body
+
+    def template(self, template_name, dict):
+        lang = self.cp.get("list", "language")
+        if lang:
+            template_name_lang = template_name + "." + lang
+        else:
+            template_name_lang = template_name
+
+        if not dict.has_key("list"):
+            dict["list"] = self.name
+            dict["local"], dict["domain"] = self.name.split("@")
+        if not dict.has_key("list"):
+            dict["list"] = self.name
+
+        for dir in [os.path.join(self.dirname, "templates")] + TEMPLATE_DIRS:
+            pathname = os.path.join(dir, template_name_lang)
+            if not os.path.exists(pathname):
+                pathname = os.path.join(dir, template_name)
+            if os.path.exists(pathname):
+                f = open(pathname, "r")
+                data = f.read()
+                f.close()
+                return data % dict
+
+        raise MissingTemplate(template_name)
+
+    def send_template(self, envelope_sender, sender, recipients,
+                      template_name, dict):
+        dict["From"] = "EoC <%s>" % sender
+        dict["To"] = string.join(recipients, ", ")
+        text = self.template(template_name, dict)
+        if not text:
+            return
+        if self.cp.get("list", "pristine-headers") != "yes":
+            text = self.mime_encode_headers(text)
+        self.mlm.send_mail(envelope_sender, recipients, text)
+
+    def send_info_message(self, recipients, template_name, dict):
+        self.send_template(self.command_address("ignore"),
+                           self.command_address("help"),
+                           recipients,
+                           template_name,
+                           dict)
+
+    def owners(self):
+        return self.cp.get("list", "owners").split()
+
+    def moderators(self):
+        return self.cp.get("list", "moderators").split()
+
+    def is_list_owner(self, address):
+        return address in self.owners()
+
+    def obey_help(self):
+        self.send_info_message([get_from_environ("SENDER")], "help", {})
+
+    def obey_list(self):
+        recipient = get_from_environ("SENDER")
+        if self.is_list_owner(recipient):
+            addr_list = self.subscribers.get_all()
+            addr_text = string.join(addr_list, "\n")
+            self.send_info_message([recipient], "list",
+                                   {
+                                     "addresses": addr_text,
+                                     "count": len(addr_list),
+                                   })
+        else:
+            self.send_info_message([recipient], "list-sorry", {})
+
+    def obey_setlist(self, origmail):
+        recipient = get_from_environ("SENDER")
+        if self.is_list_owner(recipient):
+            id = self.moderation_box.add(recipient, origmail)
+            if self.parse_setlist_addresses(origmail) == None:
+                self.send_bad_addresses_in_setlist(id)
+                self.moderation_box.remove(id)
+            else:
+                confirm = self.signed_address("setlistyes", id)
+                self.send_info_message(self.owners(), "setlist-confirm",
+                                       {
+                                          "confirm": confirm,
+                                          "origmail": origmail,
+                                          "boundary": self.invent_boundary(),
+                                       })
+                
+        else:
+            self.send_info_message([recipient], "setlist-sorry", {})
+
+    def obey_setlistsilently(self, origmail):
+        recipient = get_from_environ("SENDER")
+        if self.is_list_owner(recipient):
+            id = self.moderation_box.add(recipient, origmail)
+            if self.parse_setlist_addresses(origmail) == None:
+                self.send_bad_addresses_in_setlist(id)
+                self.moderation_box.remove(id)
+            else:
+                confirm = self.signed_address("setlistsilentyes", id)
+                self.send_info_message(self.owners(), "setlist-confirm",
+                                       {
+                                          "confirm": confirm,
+                                          "origmail": origmail,
+                                          "boundary": self.invent_boundary(),
+                                       })
+        else:
+            self.info_message([recipient], "setlist-sorry", {})
+
+    def parse_setlist_addresses(self, text):
+        body = text.split("\n\n", 1)[1]
+        lines = body.split("\n")
+        lines = filter(lambda line: line != "", lines)
+        badlines = filter(lambda line: "@" not in line, lines)
+        if badlines:
+            return None
+        else:
+            return lines
+
+    def send_bad_addresses_in_setlist(self, id):
+        addr = self.moderation_box.get_address(id)
+        origmail = self.moderation_box.get(id)
+        self.send_info_message([addr], "setlist-badlist",
+                               {
+                                "origmail": origmail,
+                                "boundary": self.invent_boundary(),
+                               })
+
+
+    def obey_setlistyes(self, dict):
+        if self.moderation_box.has(dict["id"]):
+            text = self.moderation_box.get(dict["id"])
+            addresses = self.parse_setlist_addresses(text)
+            if addresses == None:
+                self.send_bad_addresses_in_setlist(id)
+            else:
+                removed_subscribers = []
+                self.subscribers.lock()
+                old = self.subscribers.get_all()
+                for address in old:
+                    if address.lower() not in map(string.lower, addresses):
+                        self.subscribers.remove(address)
+                        removed_subscribers.append(address)
+                    else:
+                        for x in addresses:
+                            if x.lower() == address.lower():
+                                addresses.remove(x)
+                self.subscribers.add_many(addresses)
+                self.subscribers.save()
+                
+                for recipient in addresses:
+                    self.send_info_message([recipient], "sub-welcome", {})
+                for recipient in removed_subscribers:
+                    self.send_info_message([recipient], "unsub-goodbye", {})
+                self.send_info_message(self.owners(), "setlist-done", {})
+
+            self.moderation_box.remove(dict["id"])
+
+    def obey_setlistsilentyes(self, dict):
+        if self.moderation_box.has(dict["id"]):
+            text = self.moderation_box.get(dict["id"])
+            addresses = self.parse_setlist_addresses(text)
+            if addresses == None:
+                self.send_bad_addresses_in_setlist(id)
+            else:
+                self.subscribers.lock()
+                old = self.subscribers.get_all()
+                for address in old:
+                    if address not in addresses:
+                        self.subscribers.remove(address)
+                    else:
+                        addresses.remove(address)
+                self.subscribers.add_many(addresses)
+                self.subscribers.save()
+                self.send_info_message(self.owners(), "setlist-done", {})
+
+            self.moderation_box.remove(dict["id"])
+
+    def obey_owner(self, text):
+        sender = get_from_environ("SENDER")
+        recipients = self.cp.get("list", "owners").split()
+        self.mlm.send_mail(sender, recipients, text)
+
+    def obey_subscribe_or_unsubscribe(self, dict, template_name, command, 
+                                      origmail):
+
+        requester  = get_from_environ("SENDER")
+        subscriber = dict["sender"]
+        if not subscriber:
+            subscriber = requester
+        if subscriber.find("@") == -1:
+            info("Trying to (un)subscribe address without @: %s" % subscriber)
+            return
+        if self.cp.get("list", "ignore-bounce") == "yes":
+            info("Will not (un)subscribe address: %s from static list" %subscriber)
+            return
+        if requester in self.owners():
+            confirmers = self.owners()
+        else:
+            confirmers = [subscriber]
+
+        id = self.subscription_box.add(subscriber, origmail)
+        confirm = self.signed_address(command, id)
+        self.send_info_message(confirmers, template_name,
+                               {
+                                    "confirm": confirm,
+                                    "origmail": origmail,
+                                    "boundary": self.invent_boundary(),
+                               })
+
+    def obey_subscribe(self, dict, origmail):
+        self.obey_subscribe_or_unsubscribe(dict, "sub-confirm", "subyes", 
+                                           origmail)
+
+    def obey_unsubscribe(self, dict, origmail):
+        self.obey_subscribe_or_unsubscribe(dict, "unsub-confirm", "unsubyes",
+                                           origmail)
+
+    def obey_subyes(self, dict):
+        if self.subscription_box.has(dict["id"]):
+            if self.cp.get("list", "subscription") == "free":
+                recipient = self.subscription_box.get_address(dict["id"])
+                self.subscribers.lock()
+                self.subscribers.add(recipient)
+                self.subscribers.save()
+                sender = self.command_address("help")
+                self.send_template(self.ignore(), sender, [recipient], 
+                                   "sub-welcome", {})
+                self.subscription_box.remove(dict["id"])
+                if self.cp.get("list", "mail-on-subscription-changes")=="yes":
+                    self.send_info_message(self.owners(), 
+                                           "sub-owner-notification",
+                                           {
+                                            "address": recipient,
+                                           })
+            else:
+                recipients = self.cp.get("list", "owners").split()
+                confirm = self.signed_address("subapprove", dict["id"])
+                deny = self.signed_address("subreject", dict["id"])
+                subscriber = self.subscription_box.get_address(dict["id"])
+                origmail = self.subscription_box.get(dict["id"])
+                self.send_template(self.ignore(), deny, recipients, 
+                                   "sub-moderate", 
+                                   {
+                                       "confirm": confirm,
+                                       "deny": deny,
+                                       "subscriber": subscriber,
+                                       "origmail": origmail,
+                                       "boundary": self.invent_boundary(),
+                                   })
+                recipient = self.subscription_box.get_address(dict["id"])
+                self.send_info_message([recipient], "sub-wait", {})
+
+    def obey_subapprove(self, dict):
+        if self.subscription_box.has(dict["id"]):
+            recipient = self.subscription_box.get_address(dict["id"])
+            self.subscribers.lock()
+            self.subscribers.add(recipient)
+            self.subscribers.save()
+            self.send_info_message([recipient], "sub-welcome", {})
+            self.subscription_box.remove(dict["id"])
+            if self.cp.get("list", "mail-on-subscription-changes")=="yes":
+                self.send_info_message(self.owners(), "sub-owner-notification",
+                                       {
+                                        "address": recipient,
+                                       })
+
+    def obey_subreject(self, dict):
+        if self.subscription_box.has(dict["id"]):
+            recipient = self.subscription_box.get_address(dict["id"])
+            self.send_info_message([recipient], "sub-reject", {})
+            self.subscription_box.remove(dict["id"])
+
+    def obey_unsubyes(self, dict):
+        if self.subscription_box.has(dict["id"]):
+            recipient = self.subscription_box.get_address(dict["id"])
+            self.subscribers.lock()
+            self.subscribers.remove(recipient)
+            self.subscribers.save()
+            self.send_info_message([recipient], "unsub-goodbye", {})
+            self.subscription_box.remove(dict["id"])
+            if self.cp.get("list", "mail-on-subscription-changes")=="yes":
+                self.send_info_message(self.owners(),
+                                       "unsub-owner-notification",
+                                       {
+                                        "address": recipient,
+                                       })
+
+    def store_into_archive(self, text):
+        if self.cp.get("list", "archived") == "yes":
+            archdir = os.path.join(self.dirname, "archive")
+            if not os.path.exists(archdir):
+                os.mkdir(archdir, 0700)
+            id = md5sum_as_hex(text)
+            f = open(os.path.join(archdir, id), "w")
+            f.write(text)
+            f.close()
+
+    def list_headers(self):
+        local, domain = self.name.split("@")
+        list = []
+        list.append("List-Id: <%s.%s>" % (local, domain))
+        list.append("List-Help: <mailto:%s-help@%s>" % (local, domain))
+        list.append("List-Unsubscribe: <mailto:%s-unsubscribe@%s>" % 
+                    (local, domain))
+        list.append("List-Subscribe: <mailto:%s-subscribe@%s>" % 
+                    (local, domain))
+        list.append("List-Post: <mailto:%s@%s>" % (local, domain))
+        list.append("List-Owner: <mailto:%s-owner@%s>" % (local, domain))
+        list.append("Precedence: bulk");
+        return string.join(list, "\n") + "\n"
+
+    def read_file(self, basename):
+        try:
+            f = open(os.path.join(self.dirname, basename), "r")
+            data = f.read()
+            f.close()
+            return data
+        except IOError:
+            return ""
+
+    def headers_to_add(self):
+        headers_to_add = self.read_file("headers-to-add").rstrip()
+        if headers_to_add:
+            return headers_to_add + "\n"
+        else:
+            return ""
+
+    def remove_some_headers(self, mail, headers_to_remove):
+        endpos = mail.find("\n\n")
+        if endpos == -1:
+            endpos = mail.find("\n\r\n")
+            if endpos == -1:
+                return mail
+        headers = mail[:endpos].split("\n")
+        body = mail[endpos:]
+    
+        remaining = []
+        add_continuation_lines = 0
+        for header in headers:
+            pos = header.find(":")
+            if pos == -1:
+                if add_continuation_lines:
+                    remaining.append(header)
+            else:
+                name = header[:pos].lower()
+                if name in headers_to_remove:
+                    add_continuation_lines = 0
+                else:
+                    add_continuation_lines = 1
+                    remaining.append(header)
+        
+        return "\n".join(remaining) + body
+
+    def headers_to_remove(self, text):
+        headers_to_remove = self.read_file("headers-to-remove").split("\n")
+        headers_to_remove = map(lambda s: s.strip().lower(), 
+                                headers_to_remove)
+        return self.remove_some_headers(text, headers_to_remove)
+
+    def append_footer(self, text):
+        if "base64" in text or "BASE64" in text:
+            import StringIO
+            for line in StringIO.StringIO(text):
+                if line.lower.beginswith("content-transfer-encoding:") and \
+                   "base64" in line.lower():
+                    return text
+        return text + self.template("footer", {})
+
+    def send_mail_to_subscribers(self, text):
+        text = self.headers_to_add() + self.list_headers() + \
+               self.headers_to_remove(text)
+        text = self.append_footer(text)
+        text, = self.mlm.call_plugins("send_mail_to_subscribers_hook",
+                                     self, text)
+        if have_email_module and \
+           self.cp.get("list", "pristine-headers") != "yes":
+            text = self.mime_encode_headers(text)
+        self.store_into_archive(text)
+        for group in self.subscribers.groups():
+            bounce = self.signed_address("bounce", group)
+            addresses = self.subscribers.in_group(group)
+            self.mlm.send_mail(bounce, addresses, text)
+
+    def post_into_moderate(self, poster, dict, text):
+        id = self.moderation_box.add(poster, text)
+        recipients = self.moderators()
+        if recipients == []:
+            recipients = self.owners()
+
+        confirm = self.signed_address("approve", id)
+        deny = self.signed_address("reject", id)
+        self.send_template(self.ignore(), deny, recipients, "msg-moderate",
+                           {
+                            "confirm": confirm,
+                            "deny": deny,
+                            "origmail": text,
+                            "boundary": self.invent_boundary(),
+                           })
+        self.send_info_message([poster], "msg-wait", {})
+    
+    def should_be_moderated(self, posting, poster):
+        if posting == "moderated":
+            return 1
+        if posting == "auto":
+            if poster.lower() not in \
+                map(string.lower, self.subscribers.get_all()):
+                return 1
+        return 0
+
+    def obey_post(self, dict, text):
+        if dict.has_key("force-moderation") and dict["force-moderation"]:
+            force_moderation = 1
+        else:
+            force_moderation = 0
+        if dict.has_key("force-posting") and dict["force-posting"]:
+            force_posting = 1
+        else:
+            force_posting = 0
+        posting = self.cp.get("list", "posting")
+        if posting not in self.posting_opts:
+            error("You have a weird 'posting' config. Please, review it")
+        poster = get_from_environ("SENDER")
+        if force_moderation:
+            self.post_into_moderate(poster, dict, text)
+        elif force_posting:
+            self.send_mail_to_subscribers(text)
+        elif self.should_be_moderated(posting, poster):
+            self.post_into_moderate(poster, dict, text)
+        else:
+            self.send_mail_to_subscribers(text)
+    def obey_approve(self, dict):
+        if self.moderation_box.lock(dict["id"]):
+            if self.moderation_box.has(dict["id"]):
+                text = self.moderation_box.get(dict["id"])
+                self.send_mail_to_subscribers(text)
+                self.moderation_box.remove(dict["id"])
+            self.moderation_box.unlock(dict["id"])
+
+    def obey_reject(self, dict):
+        if self.moderation_box.lock(dict["id"]):
+            if self.moderation_box.has(dict["id"]):
+                self.moderation_box.remove(dict["id"])
+            self.moderation_box.unlock(dict["id"])
+
+    def split_address_list(self, addrs):
+        domains = {}
+        for addr in addrs:
+            userpart, domain = addr.split("@")
+            if domains.has_key(domain):
+                domains[domain].append(addr)
+            else:
+                domains[domain] = [addr]
+        result = []
+        if len(domains.keys()) == 1:
+            for addr in addrs:
+                result.append([addr])
+        else:
+            result = domains.values()
+        return result
+
+    def obey_bounce(self, dict, text):
+        if self.subscribers.has_group(dict["id"]):
+            self.subscribers.lock()
+            addrs = self.subscribers.in_group(dict["id"])
+            if len(addrs) == 1:
+                if self.cp.get("list", "ignore-bounce") == "yes":
+                    info("Address <%s> bounced, ignoring bounce as configured." %
+                         addrs[0])
+                    self.subscribers.unlock()
+                    return
+                debug("Address <%s> bounced, setting state to bounce." %
+                      addrs[0])
+                bounce_id = self.bounce_box.add(addrs[0], text[:4096])
+                self.subscribers.set(dict["id"], "status", "bounced")
+                self.subscribers.set(dict["id"], "timestamp-bounced", 
+                                     "%f" % time.time())
+                self.subscribers.set(dict["id"], "bounce-id",
+                                     bounce_id)
+            else:
+                debug("Group %s bounced, splitting." % dict["id"])
+                for new_addrs in self.split_address_list(addrs):
+                    self.subscribers.add_many(new_addrs)
+                self.subscribers.remove_group(dict["id"])
+            self.subscribers.save()
+        else:
+            debug("Ignoring bounce, group %s doesn't exist (anymore?)." %
+                  dict["id"])
+
+    def obey_probe(self, dict, text):
+        id = dict["id"]
+        if self.subscribers.has_group(id):
+            self.subscribers.lock()
+            if self.subscribers.get(id, "status") == "probed":
+                self.subscribers.set(id, "status", "probebounced")
+            self.subscribers.save()
+
+    def obey(self, dict):
+        text = self.read_stdin()
+
+        if dict["command"] in ["help", "list", "subscribe", "unsubscribe",
+                               "subyes", "subapprove", "subreject",
+                               "unsubyes", "post", "approve"]:
+            sender = get_from_environ("SENDER")
+            if not sender:
+                debug("Ignoring bounce message for %s command." % 
+                        dict["command"])
+                return
+
+        if dict["command"] == "help":
+            self.obey_help()
+        elif dict["command"] == "list":
+            self.obey_list()
+        elif dict["command"] == "owner":
+            self.obey_owner(text)
+        elif dict["command"] == "subscribe":
+            self.obey_subscribe(dict, text)
+        elif dict["command"] == "unsubscribe":
+            self.obey_unsubscribe(dict, text)
+        elif dict["command"] == "subyes":
+            self.obey_subyes(dict)
+        elif dict["command"] == "subapprove":
+            self.obey_subapprove(dict)
+        elif dict["command"] == "subreject":
+            self.obey_subreject(dict)
+        elif dict["command"] == "unsubyes":
+            self.obey_unsubyes(dict)
+        elif dict["command"] == "post":
+            self.obey_post(dict, text)
+        elif dict["command"] == "approve":
+            self.obey_approve(dict)
+        elif dict["command"] == "reject":
+            self.obey_reject(dict)
+        elif dict["command"] == "bounce":
+            self.obey_bounce(dict, text)
+        elif dict["command"] == "probe":
+            self.obey_probe(dict, text)
+        elif dict["command"] == "setlist":
+            self.obey_setlist(text)
+        elif dict["command"] == "setlistsilently":
+            self.obey_setlistsilently(text)
+        elif dict["command"] == "setlistyes":
+            self.obey_setlistyes(dict)
+        elif dict["command"] == "setlistsilentyes":
+            self.obey_setlistsilentyes(dict)
+        elif dict["command"] == "ignore":
+            pass
+
+    def get_bounce_text(self, id):
+        bounce_id = self.subscribers.get(id, "bounce-id")
+        if self.bounce_box.has(bounce_id):
+            bounce_text = self.bounce_box.get(bounce_id)
+            bounce_text = string.join(map(lambda s: "> " + s + "\n",
+                                          bounce_text.split("\n")), "")
+        else:
+            bounce_text = "Bounce message not available."
+        return bounce_text
+
+    one_week = 7.0 * 24.0 * 60.0 * 60.0
+
+    def handle_bounced_groups(self, now):
+        for id in self.subscribers.groups():
+            status = self.subscribers.get(id, "status") 
+            t = float(self.subscribers.get(id, "timestamp-bounced")) 
+            if status == "bounced":
+                if now - t > self.one_week:
+                    sender = self.signed_address("probe", id) 
+                    recipients = self.subscribers.in_group(id) 
+                    self.send_template(sender, sender, recipients,
+                                       "bounce-warning", {
+                                        "bounce": self.get_bounce_text(id),
+                                        "boundary": self.invent_boundary(),
+                                       })
+                    self.subscribers.set(id, "status", "probed")
+            elif status == "probed":
+                if now - t > 2 * self.one_week:
+                    debug(("Cleaning woman: probe didn't bounce " + 
+                          "for group <%s>, setting status to ok.") % id)
+                    self.subscribers.set(id, "status", "ok")
+                    self.bounce_box.remove(
+                            self.subscribers.get(id, "bounce-id"))
+            elif status == "probebounced":
+                sender = self.command_address("help") 
+                for address in self.subscribers.in_group(id):
+                    if self.cp.get("list", "mail-on-forced-unsubscribe") \
+                        == "yes":
+                        self.send_template(sender, sender,
+                                       self.owners(),
+                                       "bounce-owner-notification",
+                                       {
+                                        "address": address,
+                                        "bounce": self.get_bounce_text(id),
+                                        "boundary": self.invent_boundary(),
+                                       })
+
+                    self.bounce_box.remove(
+                            self.subscribers.get(id, "bounce-id"))
+                    self.subscribers.remove(address) 
+                    debug("Cleaning woman: removing <%s>." % address)
+                    self.send_template(sender, sender, [address],
+                                       "bounce-goodbye", {})
+
+    def join_nonbouncing_groups(self, now):
+        to_be_joined = []
+        for id in self.subscribers.groups():
+            status = self.subscribers.get(id, "status")
+            age1 = now - float(self.subscribers.get(id, "timestamp-bounced"))
+            age2 = now - float(self.subscribers.get(id, "timestamp-created"))
+            if status == "ok":
+                if age1 > self.one_week and age2 > self.one_week:
+                    to_be_joined.append(id)
+        if to_be_joined:
+            addrs = []
+            for id in to_be_joined:
+                addrs = addrs + self.subscribers.in_group(id)
+            self.subscribers.add_many(addrs)
+            for id in to_be_joined:
+                self.bounce_box.remove(self.subscribers.get(id, "bounce-id"))
+                self.subscribers.remove_group(id)
+
+    def remove_empty_groups(self):
+        for id in self.subscribers.groups()[:]:
+            if len(self.subscribers.in_group(id)) == 0:
+                self.subscribers.remove_group(id)
+
+    def cleaning_woman(self, now):
+        if self.subscribers.lock():
+            self.handle_bounced_groups(now)
+            self.join_nonbouncing_groups(now)
+            self.subscribers.save()
+
+class SubscriberDatabase:
+
+    def __init__(self, dirname, name):
+        self.dict = {}
+        self.filename = os.path.join(dirname, name)
+        self.lockname = os.path.join(dirname, "lock")
+        self.loaded = 0
+        self.locked = 0
+
+    def lock(self):
+        if os.system("lockfile -l 60 %s" % self.lockname) == 0:
+            self.locked = 1
+            self.load()
+        return self.locked
+    
+    def unlock(self):
+        os.remove(self.lockname)
+        self.locked = 0
+    
+    def load(self):
+        if not self.loaded and not self.dict:
+            f = open(self.filename, "r")
+            for line in f.xreadlines():
+                parts = line.split()
+                self.dict[parts[0]] = {
+                    "status": parts[1],
+                    "timestamp-created": parts[2],
+                    "timestamp-bounced": parts[3],
+                    "bounce-id": parts[4],
+                    "addresses": parts[5:],
+                }
+            f.close()
+            self.loaded = 1
+
+    def save(self):
+        assert self.locked
+        assert self.loaded
+        f = open(self.filename + ".new", "w")
+        for id in self.dict.keys():
+            f.write("%s " % id)
+            f.write("%s " % self.dict[id]["status"])
+            f.write("%s " % self.dict[id]["timestamp-created"])
+            f.write("%s " % self.dict[id]["timestamp-bounced"])
+            f.write("%s " % self.dict[id]["bounce-id"])
+            f.write("%s\n" % string.join(self.dict[id]["addresses"], " "))
+        f.close()
+        os.remove(self.filename)
+        os.rename(self.filename + ".new", self.filename)
+        self.unlock()
+
+    def get(self, id, attribute):
+        self.load()
+        if self.dict.has_key(id) and self.dict[id].has_key(attribute):
+            return self.dict[id][attribute]
+        return None
+
+    def set(self, id, attribute, value):
+        assert self.locked
+        self.load()
+        if self.dict.has_key(id) and self.dict[id].has_key(attribute):
+            self.dict[id][attribute] = value
+
+    def add(self, address):
+        return self.add_many([address])
+
+    def add_many(self, addresses):
+        assert self.locked
+        assert self.loaded
+        for addr in addresses[:]:
+            if addr.find("@") == -1:
+                info("Address '%s' does not contain an @, ignoring it." % addr)
+                addresses.remove(addr)
+        for id in self.dict.keys():
+            old_ones = self.dict[id]["addresses"]
+            for addr in addresses:
+                for x in old_ones:
+                    if x.lower() == addr.lower():
+                        old_ones.remove(x)
+            self.dict[id]["addresses"] = old_ones
+        id = self.new_group()
+        self.dict[id] = {
+            "status": "ok",
+            "timestamp-created": self.timestamp(),
+            "timestamp-bounced": "0",
+            "bounce-id": "..notexist..",
+            "addresses": addresses,
+        }
+        return id
+
+    def new_group(self):
+        keys = self.dict.keys()
+        if keys:
+            keys = map(lambda x: int(x), keys)
+            keys.sort()
+            return "%d" % (keys[-1] + 1)
+        else:
+            return "0"
+
+    def timestamp(self):
+        return "%.0f" % time.time()
+
+    def get_all(self):
+        self.load()
+        list = []
+        for values in self.dict.values():
+            list = list + values["addresses"]
+        return list
+
+    def groups(self):
+        self.load()
+        return self.dict.keys()
+
+    def has_group(self, id):
+        self.load()
+        return self.dict.has_key(id)
+
+    def in_group(self, id):
+        self.load()
+        return self.dict[id]["addresses"]
+
+    def remove(self, address):
+        assert self.locked
+        self.load()
+        for id in self.dict.keys():
+            group = self.dict[id]
+            for x in group["addresses"][:]:
+                if x.lower() == address.lower():
+                    group["addresses"].remove(x)
+                    if len(group["addresses"]) == 0:
+                        del self.dict[id]
+
+    def remove_group(self, id):
+        assert self.locked
+        self.load()
+        del self.dict[id]
+
+
+class MessageBox:
+
+    def __init__(self, dirname, boxname):
+        self.boxdir = os.path.join(dirname, boxname)
+        if not os.path.isdir(self.boxdir):
+            os.mkdir(self.boxdir, 0700)
+
+    def filename(self, id):
+        return os.path.join(self.boxdir, id)
+
+    def add(self, address, message_text):
+        id = self.make_id(message_text)
+        filename = self.filename(id)
+        f = open(filename + ".address", "w")
+        f.write(address)
+        f.close()
+        f = open(filename + ".new", "w")
+        f.write(message_text)
+        f.close()
+        os.rename(filename + ".new", filename)
+        return id
+
+    def make_id(self, message_text):
+        return md5sum_as_hex(message_text)
+        # XXX this might be unnecessarily long
+
+    def remove(self, id):
+        filename = self.filename(id)
+        if os.path.isfile(filename):
+            os.remove(filename)
+            os.remove(filename + ".address")
+
+    def has(self, id):
+        return os.path.isfile(self.filename(id))
+
+    def get_address(self, id):
+        f = open(self.filename(id) + ".address", "r")
+        data = f.read()
+        f.close()
+        return data.strip()
+
+    def get(self, id):
+        f = open(self.filename(id), "r")
+        data = f.read()
+        f.close()
+        return data
+
+    def lockname(self, id):
+        return self.filename(id) + ".lock"
+
+    def lock(self, id):
+        if os.system("lockfile -l 600 %s" % self.lockname(id)) == 0:
+            return 1
+        else:
+            return 0
+    
+    def unlock(self, id):
+        try:
+            os.remove(self.lockname(id))
+        except os.error:
+            pass
+    
+
+
+class DevNull:
+
+    def write(self, str):
+        pass
+
+
+log_file_handle = None
+def log_file():
+    global log_file_handle
+    if log_file_handle is None:
+        try:
+            log_file_handle = open(os.path.join(DOTDIR, "logfile.txt"), "a")
+        except:
+            log_file_handle = DevNull()
+    return log_file_handle
+
+def timestamp():
+    tuple = time.localtime(time.time())
+    return time.strftime("%Y-%m-%d %H:%M:%S", tuple) + " [%d]" % os.getpid()
+
+
+quiet = 0
+
+
+# No logging to stderr of debug messages. Some MTAs have a limit on how
+# much data they accept via stderr and debug logs will fill that quickly.
+def debug(msg):
+    log_file().write(timestamp() + " " + msg + "\n")
+
+
+# Log to log file first, in case MTA's stderr buffer fills up and we lose
+# logs.
+def info(msg):
+    log_file().write(timestamp() + " " + msg + "\n")
+    sys.stderr.write(msg + "\n")
+
+
+def error(msg):
+    info(msg)
+    sys.exit(1)
+
+
+def usage():
+    sys.stdout.write("""\
+Usage: enemies-of-carlotta [options] command
+Mailing list manager.
+
+Options:
+  --name=listname@domain
+  --owner=address@domain
+  --moderator=address@domain
+  --subscription=free/moderated
+  --posting=free/moderated/auto
+  --archived=yes/no
+  --ignore-bounce=yes/no
+  --language=language code or empty
+  --mail-on-forced-unsubscribe=yes/no
+  --mail-on-subscription-changes=yes/no
+  --skip-prefix=string
+  --domain=domain.name
+  --smtp-server=domain.name
+  --quiet
+  --moderate
+
+Commands:
+  --help
+  --create
+  --subscribe
+  --unsubscribe
+  --list
+  --is-list
+  --edit
+  --incoming
+  --cleaning-woman
+  --show-lists
+
+For more detailed information, please read the enemies-of-carlotta(1)
+manual page.
+""")
+    sys.exit(0)
+
+
+def no_act_send_mail(sender, recipients, text):
+    print "NOT SENDING MAIL FOR REAL!"
+    print "Sender:", sender
+    print "Recipients:", recipients
+    print "Mail:"
+    print "\n".join(map(lambda s: "   " + s, text.split("\n")))
+
+
+def set_list_options(list, owners, moderators, subscription, posting, 
+                     archived, language, ignore_bounce,
+                     mail_on_sub_changes, mail_on_forced_unsub):
+    if owners:
+        list.cp.set("list", "owners", string.join(owners, " "))
+    if moderators:
+        list.cp.set("list", "moderators", string.join(moderators, " "))
+    if subscription != None:
+        list.cp.set("list", "subscription", subscription)
+    if posting != None:
+        list.cp.set("list", "posting", posting)
+    if archived != None:
+        list.cp.set("list", "archived", archived)
+    if language != None:
+        list.cp.set("list", "language", language)
+    if ignore_bounce != None:
+        list.cp.set("list", "ignore-bounce", ignore_bounce)
+    if mail_on_sub_changes != None:
+        list.cp.set("list", "mail-on-subscription-changes", 
+                            mail_on_sub_changes)
+    if mail_on_forced_unsub != None:
+        list.cp.set("list", "mail-on-forced-unsubscribe",
+                            mail_on_forced_unsub)
+
+
+def main(args):
+    try:
+        opts, args = getopt.getopt(args, "h",
+                                   ["name=",
+                                    "owner=",
+                                    "moderator=",
+                                    "subscription=",
+                                    "posting=",
+                                    "archived=",
+                                    "language=",
+                                    "ignore-bounce=",
+                                    "mail-on-forced-unsubscribe=",
+                                    "mail-on-subscription-changes=",
+                                    "skip-prefix=",
+                                    "domain=",
+                                    "sendmail=",
+                                    "smtp-server=",
+                                    "qmqp-server=",
+                                    "quiet",
+                                    "moderate",
+                                    "post",
+                                    "sender=",
+                                    "recipient=",
+                                    "no-act",
+                                    
+                                    "set",
+                                    "get",
+                                    "help",
+                                    "create",
+                                    "destroy",
+                                    "subscribe",
+                                    "unsubscribe",
+                                    "list",
+                                    "is-list",
+                                    "edit",
+                                    "incoming",
+                                    "cleaning-woman",
+                                    "show-lists",
+                                    "version",
+                                   ])
+    except getopt.GetoptError, detail:
+        error("Error parsing command line options (see --help):\n%s" % 
+              detail)
+
+    operation = None
+    list_name = None
+    owners = []
+    moderators = []
+    subscription = None
+    posting = None
+    archived = None
+    ignore_bounce = None
+    skip_prefix = None
+    domain = None
+    sendmail = "/usr/sbin/sendmail"
+    smtp_server = None
+    qmqp_server = None
+    moderate = 0
+    post = 0
+    sender = None
+    recipient = None
+    language = None
+    mail_on_forced_unsub = None
+    mail_on_sub_changes = None
+    no_act = 0
+    global quiet
+
+    for opt, arg in opts:
+        if opt == "--name":
+            list_name = arg
+        elif opt == "--owner":
+            owners.append(arg)
+        elif opt == "--moderator":
+            moderators.append(arg)
+        elif opt == "--subscription":
+            subscription = arg
+        elif opt == "--posting":
+            posting = arg
+        elif opt == "--archived":
+            archived = arg
+        elif opt == "--ignore-bounce":
+            ignore_bounce = arg
+        elif opt == "--skip-prefix":
+            skip_prefix = arg
+        elif opt == "--domain":
+            domain = arg
+        elif opt == "--sendmail":
+            sendmail = arg
+        elif opt == "--smtp-server":
+            smtp_server = arg
+        elif opt == "--qmqp-server":
+            qmqp_server = arg
+        elif opt == "--sender":
+            sender = arg
+        elif opt == "--recipient":
+            recipient = arg
+        elif opt == "--language":
+            language = arg
+        elif opt == "--mail-on-forced-unsubscribe":
+            mail_on_forced_unsub = arg
+        elif opt == "--mail-on-subscription-changes":
+            mail_on_sub_changes = arg
+        elif opt == "--moderate":
+            moderate = 1
+        elif opt == "--post":
+            post = 1
+        elif opt == "--quiet":
+            quiet = 1
+        elif opt == "--no-act":
+            no_act = 1
+        else:
+            operation = opt
+
+    if operation is None:
+        error("No operation specified, see --help.")
+
+    if list_name is None and operation not in ["--incoming", "--help", "-h",
+                                               "--cleaning-woman",
+                                               "--show-lists",
+                                               "--version"]:
+        error("%s requires a list name specified with --name" % operation)
+
+    if operation in ["--help", "-h"]:
+        usage()
+
+    if sender or recipient:
+        environ = os.environ.copy()
+        if sender:
+            environ["SENDER"] = sender
+        if recipient:
+            environ["RECIPIENT"] = recipient
+        set_environ(environ)
+
+    mlm = MailingListManager(DOTDIR, sendmail=sendmail, 
+                             smtp_server=smtp_server,
+                             qmqp_server=qmqp_server)
+    if no_act:
+        mlm.send_mail = no_act_send_mail
+
+    if operation == "--create":
+        if not owners:
+            error("You must give at least one list owner with --owner.")
+        list = mlm.create_list(list_name)
+        set_list_options(list, owners, moderators, subscription, posting, 
+                         archived, language, ignore_bounce,
+                         mail_on_sub_changes, mail_on_forced_unsub)
+        list.save_config()
+        debug("Created list %s." % list_name)
+    elif operation == "--destroy":
+        shutil.rmtree(os.path.join(DOTDIR, list_name))
+        debug("Removed list %s." % list_name)
+    elif operation == "--edit":
+        list = mlm.open_list(list_name)
+        set_list_options(list, owners, moderators, subscription, posting, 
+                         archived, language, ignore_bounce,
+                         mail_on_sub_changes, mail_on_forced_unsub)
+        list.save_config()
+    elif operation == "--subscribe":
+        list = mlm.open_list(list_name)
+        list.subscribers.lock()
+        for address in args:
+            if address.find("@") == -1:
+                error("Address '%s' does not contain an @." % address)
+            list.subscribers.add(address)
+            debug("Added subscriber <%s>." % address)
+        list.subscribers.save()
+    elif operation == "--unsubscribe":
+        list = mlm.open_list(list_name)
+        list.subscribers.lock()
+        for address in args:
+            list.subscribers.remove(address)
+            debug("Removed subscriber <%s>." % address)
+        list.subscribers.save()
+    elif operation == "--list":
+        list = mlm.open_list(list_name)
+        for address in list.subscribers.get_all():
+            print address
+    elif operation == "--is-list":
+        if mlm.is_list(list_name, skip_prefix, domain):
+            debug("Indeed a mailing list: <%s>" % list_name)
+        else:
+            debug("Not a mailing list: <%s>" % list_name)
+            sys.exit(1)
+    elif operation == "--incoming":
+        mlm.incoming_message(skip_prefix, domain, moderate, post)
+    elif operation == "--cleaning-woman":
+        mlm.cleaning_woman()
+    elif operation == "--show-lists":
+        listnames = mlm.get_lists()
+        listnames.sort()
+        for listname in listnames:
+            print listname
+    elif operation == "--get":
+        list = mlm.open_list(list_name)
+        for name in args:
+            print list.cp.get("list", name)
+    elif operation == "--set":
+        list = mlm.open_list(list_name)
+        for arg in args:
+            if "=" not in arg:
+                error("Error: --set arguments must be of form name=value")
+            name, value = arg.split("=", 1)
+            list.cp.set("list", name, value)
+        list.save_config()
+    elif operation == "--version":
+        print "EoC, version %s" % VERSION
+        print "Home page: http://liw.iki.fi/liw/eoc/"
+    else:
+        error("Internal error: unimplemented option <%s>." % operation)
+
+if __name__ == "__main__":
+    try:
+        main(sys.argv[1:])
+    except EocException, detail:
+        error("Error: %s" % detail)
diff --git a/eocTests.py b/eocTests.py
new file mode 100644 (file)
index 0000000..acf99b3
--- /dev/null
@@ -0,0 +1,1100 @@
+import unittest
+import shutil
+import os
+import re
+import time
+import string
+
+import eoc
+
+DOTDIR = "dot-dir-for-testing"
+eoc.DOTDIR = DOTDIR
+eoc.quiet = 1
+
+def no_op(*args):
+    pass
+
+class AddressCleaningTestCases(unittest.TestCase):
+
+    def setUp(self):
+        self.ap = eoc.AddressParser(["foo@EXAMPLE.com",
+                                     "bar@lists.example.com"])
+    
+    def verify(self, address, wanted, skip_prefix=None, forced_domain=None):
+        self.ap.set_skip_prefix(skip_prefix)
+        self.ap.set_forced_domain(forced_domain)
+        address = self.ap.clean(address)
+        self.failUnlessEqual(address, wanted)
+    
+    def testSimpleAddress(self):
+        self.verify("foo@example.com", "foo@example.com")
+
+    def testUpperCaseAddress(self):
+        self.verify("FOO@EXAMPLE.COM", "foo@example.com")
+
+    def testPrefixRemoval(self):
+        self.verify("foo@example.com", "foo@example.com", 
+                    skip_prefix="user-")
+        self.verify("user-foo@example.com", "foo@example.com", 
+                    skip_prefix="user-")
+
+    def testForcedDomain(self):
+        self.verify("foo@example.com", "foo@example.com",
+                    forced_domain="example.com")
+        self.verify("foo@whatever.example.com", "foo@example.com", 
+                    forced_domain="example.com")
+
+    def testPrefixRemovalWithForcedDomain(self):
+        self.verify("foo@example.com", "foo@example.com", 
+                    skip_prefix="user-",
+                    forced_domain="example.com")
+        self.verify("foo@whatever.example.com", "foo@example.com", 
+                    skip_prefix="user-",
+                    forced_domain="example.com")
+        self.verify("user-foo@example.com", "foo@example.com", 
+                    skip_prefix="user-",
+                    forced_domain="example.com")
+        self.verify("user-foo@whatever.example.com", "foo@example.com", 
+                    skip_prefix="user-",
+                    forced_domain="example.com")
+
+class AddressParserTestCases(unittest.TestCase):
+
+    def setUp(self):
+        self.ap = eoc.AddressParser(["foo@EXAMPLE.com",
+                                     "bar@lists.example.com"])
+    
+    def verify_parser(self, address, wanted_listname, wanted_parts):
+        listname, parts = self.ap.parse(address)
+        self.failUnlessEqual(listname, wanted_listname)
+        self.failUnlessEqual(parts, wanted_parts)
+        
+    def testParser(self):
+        self.verify_parser("foo@example.com", 
+                           "foo@EXAMPLE.com", 
+                           [])
+        self.verify_parser("foo-subscribe@example.com", 
+                           "foo@EXAMPLE.com", 
+                           ["subscribe"])
+        self.verify_parser("foo-subscribe-joe=example.com@example.com", 
+                           "foo@EXAMPLE.com", 
+                           ["subscribe", "joe=example.com"])
+        self.verify_parser("foo-bounce-123-ABCDEF@example.com", 
+                           "foo@EXAMPLE.com", 
+                           ["bounce", "123", "abcdef"])
+
+class ParseRecipientAddressBase(unittest.TestCase):
+
+    def setUp(self):
+        self.lists = ["foo@example.com", 
+                      "bar@lists.example.com",
+                      "foo-announce@example.com"]
+        self.mlm = eoc.MailingListManager(DOTDIR, lists=self.lists)
+    
+    def environ(self, sender, recipient):
+        eoc.set_environ({
+            "SENDER": sender,
+            "RECIPIENT": recipient,
+        })
+
+class ParseUnsignedAddressTestCases(ParseRecipientAddressBase):
+
+    def testEmpty(self):
+        self.failUnlessRaises(eoc.UnknownList,
+                              self.mlm.parse_recipient_address, 
+                              "", None, None)
+
+    def verify(self, address, skip_prefix, forced_domain, wanted_dict):
+        dict = self.mlm.parse_recipient_address(address, skip_prefix, 
+                                                forced_domain)
+        self.failUnlessEqual(dict, wanted_dict)
+
+    def testSimpleAddresses(self):
+        self.verify("foo@example.com", 
+                    None, 
+                    None, 
+                    { "name": "foo@example.com", "command": "post" })
+        self.verify("FOO@EXAMPLE.COM", 
+                    None, 
+                    None, 
+                    { "name": "foo@example.com", "command": "post" })
+        self.verify("prefix-foo@example.com", 
+                    "prefix-", 
+                    None, 
+                    { "name": "foo@example.com", "command": "post" })
+        self.verify("bar@example.com", 
+                    None, 
+                    "lists.example.com", 
+                    { "name": "bar@lists.example.com", "command": "post" })
+        self.verify("prefix-bar@example.com", 
+                    "prefix-",
+                    "lists.example.com", 
+                    { "name": "bar@lists.example.com", "command": "post" })
+
+    def testSubscription(self):
+        self.verify("foo-subscribe@example.com", 
+                    None,
+                    None,
+                    { "name": "foo@example.com", 
+                      "command": "subscribe",
+                      "sender": "",
+                    })
+        self.verify("foo-subscribe-joe-user=example.com@example.com", 
+                    None,
+                    None,
+                    { "name": "foo@example.com", 
+                      "command": "subscribe",
+                      "sender": "joe-user@example.com",
+                    })
+        self.verify("foo-unsubscribe@example.com", 
+                    None,
+                    None,
+                    { "name": "foo@example.com", 
+                      "command": "unsubscribe",
+                      "sender": "",
+                    })
+        self.verify("foo-unsubscribe-joe-user=example.com@example.com", 
+                    None,
+                    None,
+                    { "name": "foo@example.com", 
+                      "command": "unsubscribe",
+                      "sender": "joe-user@example.com",
+                    })
+
+    def testPost(self):
+        for name in self.lists:
+            self.verify(name, None, None, { "name": name, "command": "post" })
+
+    def testSimpleCommands(self):
+        for name in self.lists:
+            for command in ["help", "list", "owner"]:
+                localpart, domain = name.split("@")
+                address = "%s-%s@%s" % (localpart, command, domain)
+                self.verify(address, None, None,
+                            { "name": name,
+                              "command": command
+                            })
+
+class ParseWellSignedAddressTestCases(ParseRecipientAddressBase):
+
+    def try_good_signature(self, command):
+        s = "foo-announce-%s-1" % command
+        hash = self.mlm.compute_hash("%s@%s" % (s, "example.com"))
+        local_part = "%s-%s" % (s, hash)
+        dict = self.mlm.parse_recipient_address("%s@example.com" % local_part,
+                                                None, None)
+        self.failUnlessEqual(dict,
+                             {
+                                "name": "foo-announce@example.com",
+                                "command": command,
+                                "id": "1",
+                             })
+
+    def testProperlySignedCommands(self):
+        self.try_good_signature("subyes")
+        self.try_good_signature("subapprove")
+        self.try_good_signature("subreject")
+        self.try_good_signature("unsubyes")
+        self.try_good_signature("bounce")
+        self.try_good_signature("approve")
+        self.try_good_signature("reject")
+        self.try_good_signature("probe")
+
+class ParseBadlySignedAddressTestCases(ParseRecipientAddressBase):
+
+    def try_bad_signature(self, command_part):
+        self.failUnlessRaises(eoc.BadSignature,
+                              self.mlm.parse_recipient_address, 
+                              "foo-announce-" + command_part + 
+                                    "-123-badhash@example.com",
+                              None, None)
+
+    def testBadlySignedCommands(self):
+        self.try_bad_signature("subyes")
+        self.try_bad_signature("subapprove")
+        self.try_bad_signature("subreject")
+        self.try_bad_signature("unsubyes")
+        self.try_bad_signature("bounce")
+        self.try_bad_signature("approve")
+        self.try_bad_signature("reject")
+        self.try_bad_signature("probe")
+
+class DotDirTestCases(unittest.TestCase):
+
+    def setUp(self):
+        self.secret_name = os.path.join(DOTDIR, "secret")
+
+    def tearDown(self):
+        shutil.rmtree(DOTDIR)
+
+    def dotdir_is_ok(self):
+        self.failUnless(os.path.isdir(DOTDIR))
+        self.failUnless(os.path.isfile(self.secret_name))
+
+    def testNoDotDirExists(self):
+        self.failIf(os.path.exists(DOTDIR))
+        mlm = eoc.MailingListManager(DOTDIR)
+        self.dotdir_is_ok()
+
+    def testDotDirDoesExistButSecretDoesNot(self):
+        self.failIf(os.path.exists(DOTDIR))
+        os.makedirs(DOTDIR)
+        self.failUnless(os.path.isdir(DOTDIR))
+        self.failIf(os.path.exists(self.secret_name))
+        mlm = eoc.MailingListManager(DOTDIR)
+        self.dotdir_is_ok()
+
+class ListBase(unittest.TestCase):
+
+    def setUp(self):
+        if os.path.exists(DOTDIR):
+            shutil.rmtree(DOTDIR)
+        self.mlm = eoc.MailingListManager(DOTDIR)
+
+    def tearDown(self):
+        self.mlm = None
+        shutil.rmtree(DOTDIR)
+
+class ListCreationTestCases(ListBase):
+
+    def setUp(self):
+        ListBase.setUp(self)
+        self.names = None
+
+    def listdir(self, listname):
+        return os.path.join(DOTDIR, listname)
+
+    def listdir_has_file(self, listdir, filename):
+        self.failUnless(os.path.isfile(os.path.join(listdir, filename)))
+        self.names.remove(filename)
+
+    def listdir_has_dir(self, listdir, dirname):
+        self.failUnless(os.path.isdir(os.path.join(listdir, dirname)))
+        self.names.remove(dirname)
+
+    def listdir_may_have_dir(self, listdir, dirname):
+        if dirname in self.names:
+            self.listdir_has_dir(listdir, dirname)
+
+    def listdir_is_ok(self, listname):
+        listdir = self.listdir(listname)
+        self.failUnless(os.path.isdir(listdir))
+        self.names = os.listdir(listdir)
+        
+        self.listdir_has_file(listdir, "config")
+        self.listdir_has_file(listdir, "subscribers")
+            
+        self.listdir_has_dir(listdir, "bounce-box")
+        self.listdir_has_dir(listdir, "subscription-box")
+            
+        self.listdir_may_have_dir(listdir, "moderation-box")
+        self.listdir_may_have_dir(listdir, "templates")
+            
+        # Make sure there are no extras.
+        self.failUnlessEqual(self.names, [])
+
+    def testCreateNew(self):
+        self.failIf(os.path.exists(self.listdir("foo@example.com")))
+        ml = self.mlm.create_list("foo@example.com")
+
+        self.failUnlessEqual(ml.__class__, eoc.MailingList)
+        self.failUnlessEqual(ml.dirname, self.listdir("foo@example.com"))
+
+        self.listdir_is_ok("foo@example.com")
+
+    def testCreateExisting(self):
+        list = self.mlm.create_list("foo@example.com")
+        self.failUnlessRaises(eoc.ListExists,
+                              self.mlm.create_list, "foo@example.com")
+        self.listdir_is_ok("foo@example.com")
+
+class ListOptionTestCases(ListBase):
+
+    def check(self, ml, wanted):
+        self.failUnlessEqual(ml.cp.sections(), ["list"])
+        cpdict = {}
+        for key, value in ml.cp.items("list"):
+            cpdict[key] = value
+        self.failUnlessEqual(cpdict, wanted)
+
+    def testDefaultOptionsOnCreateAndOpenExisting(self):
+        self.mlm.create_list("foo@example.com")
+        ml = self.mlm.open_list("foo@example.com")
+        self.check(ml,
+                   {
+                      "owners": "",
+                      "moderators": "",
+                      "subscription": "free",
+                      "posting": "free",
+                      "archived": "no",
+                      "mail-on-subscription-changes": "no",
+                      "mail-on-forced-unsubscribe": "no",
+                      "ignore-bounce": "no",
+                      "language": "",
+                      "pristine-headers": "",
+                   })
+
+    def testChangeOptions(self):
+        # Create a list, change some options, and save the result.
+        ml = self.mlm.create_list("foo@example.com")
+        self.failUnlessEqual(ml.cp.get("list", "owners"), "")
+        self.failUnlessEqual(ml.cp.get("list", "posting"), "free")
+        ml.cp.set("list", "owners", "owner@example.com")
+        ml.cp.set("list", "posting", "moderated")
+        ml.save_config()
+        
+        # Re-open the list and check that the new instance has read the
+        # values from the disk correctly.
+        ml2 = self.mlm.open_list("foo@example.com")
+        self.check(ml2,
+                   {
+                      "owners": "owner@example.com",
+                      "moderators": "",
+                      "subscription": "free",
+                      "posting": "moderated",
+                      "archived": "no",
+                      "mail-on-subscription-changes": "no",
+                      "mail-on-forced-unsubscribe": "no",
+                      "ignore-bounce": "no",
+                      "language": "",
+                      "pristine-headers": "",
+                   })
+
+class SubscriberDatabaseTestCases(ListBase):
+
+    def has_subscribers(self, ml, addrs):
+        subs = ml.subscribers.get_all()
+        subs.sort()
+        self.failUnlessEqual(subs, addrs)
+
+    def testAddAndRemoveSubscribers(self):
+        addrs = ["joe@example.com", "MARY@example.com", "bubba@EXAMPLE.com"]
+        addrs.sort()
+    
+        ml = self.mlm.create_list("foo@example.com")
+        self.failUnlessEqual(ml.subscribers.get_all(), [])
+
+        self.failUnless(ml.subscribers.lock())
+        ml.subscribers.add_many(addrs)
+        self.has_subscribers(ml, addrs)
+        ml.subscribers.save()
+        self.failIf(ml.subscribers.locked)
+        ml = None
+
+        ml2 = self.mlm.open_list("foo@example.com")
+        self.has_subscribers(ml2, addrs)
+        ml2.subscribers.lock()
+        ml2.subscribers.remove(addrs[0])
+        self.has_subscribers(ml2, addrs[1:])
+        
+        ml2.subscribers.save()
+        
+        ml3 = self.mlm.open_list("foo@example.com")
+        self.has_subscribers(ml3, addrs[1:])
+
+    def testSubscribeTwice(self):
+        ml = self.mlm.create_list("foo@example.com")
+        self.failUnlessEqual(ml.subscribers.get_all(), [])
+        ml.subscribers.lock()
+        ml.subscribers.add("user@example.com")
+        ml.subscribers.add("USER@example.com")
+        self.failUnlessEqual(map(string.lower, ml.subscribers.get_all()),
+                             map(string.lower, ["USER@example.com"]))
+
+    def testSubscriberAttributesAndGroups(self):
+        addrs = ["joe@example.com", "mary@example.com"]
+        addrs.sort()
+        ml = self.mlm.create_list("foo@example.com")
+        self.failUnlessEqual(ml.subscribers.groups(), [])
+        ml.subscribers.lock()
+        id = ml.subscribers.add_many(addrs)
+        self.failUnlessEqual(ml.subscribers.groups(), ["0"])
+        self.failUnlessEqual(ml.subscribers.get(id, "status"), "ok")
+        ml.subscribers.set(id, "status", "bounced")
+        self.failUnlessEqual(ml.subscribers.get(id, "status"), "bounced")
+        subs = ml.subscribers.in_group(id)
+        subs.sort()
+        self.failUnlessEqual(subs, addrs)
+
+class ModerationBoxTestCases(ListBase):
+
+    def testModerationBox(self):
+        ml = self.mlm.create_list("foo@example.com")
+        listdir = os.path.join(DOTDIR, "foo@example.com")
+        boxdir = os.path.join(listdir, "moderation-box")
+
+        self.failUnlessEqual(boxdir, ml.moderation_box.boxdir)
+        self.failUnless(os.path.isdir(boxdir))
+
+        mailtext = "From: foo\nTo: bar\n\nhello\n"
+        id = ml.moderation_box.add("foo", mailtext)
+        self.failUnless(ml.moderation_box.has(id))
+        self.failUnlessEqual(ml.moderation_box.get_address(id), "foo")
+        self.failUnlessEqual(ml.moderation_box.get(id), mailtext)
+        
+        filename = os.path.join(boxdir, id)
+        self.failUnless(os.path.isfile(filename))
+        self.failUnless(os.path.isfile(filename + ".address"))
+        
+        ml.moderation_box.remove(id)
+        self.failIf(ml.moderation_box.has(id))
+        self.failUnless(not os.path.exists(filename))
+
+class IncomingBase(unittest.TestCase):
+
+    def setUp(self):
+        if os.path.isdir(DOTDIR):
+            shutil.rmtree(DOTDIR)
+        self.mlm = eoc.MailingListManager(DOTDIR)
+        self.ml = None
+        ml = self.mlm.create_list("foo@EXAMPLE.com")
+        ml.cp.set("list", "owners", "listmaster@example.com")
+        ml.save_config()
+        ml.subscribers.lock()
+        ml.subscribers.add("USER1@example.com")
+        ml.subscribers.add("user2@EXAMPLE.com")
+        ml.subscribers.save()
+        self.write_file_in_listdir(ml, "headers-to-add", "X-Foo: foo\n")
+        self.write_file_in_listdir(ml, "headers-to-remove", "Received\n")
+        self.sent_mail = []
+
+    def tearDown(self):
+        shutil.rmtree(DOTDIR)
+
+    def write_file_in_listdir(self, ml, basename, contents):
+        f = open(os.path.join(ml.dirname, basename), "w")
+        f.write(contents)
+        f.close()
+
+    def configure_list(self, subscription, posting):
+        list = self.mlm.open_list("foo@example.com")
+        list.cp.set("list", "subscription", subscription)
+        list.cp.set("list", "posting", posting)
+        list.save_config()
+
+    def environ(self, sender, recipient):
+        eoc.set_environ({
+            "SENDER": sender,
+            "RECIPIENT": recipient,
+        })
+
+    def catch_sendmail(self, sender, recipients, text):
+        self.sent_mail.append({
+            "sender": sender,
+            "recipients": recipients,
+            "text": text,
+        })
+
+    def send(self, sender, recipient, text="", force_moderation=0, 
+             force_posting=0):
+        self.environ(sender, recipient)
+        dict = self.mlm.parse_recipient_address(recipient, None, None)
+        dict["force-moderation"] = force_moderation
+        dict["force-posting"] = force_posting
+        self.ml = self.mlm.open_list(dict["name"])
+        if "\n\n" not in text:
+            text = "\n\n" + text
+        text = "Received: foobar\n" + text
+        self.ml.read_stdin = lambda t=text: t
+        self.mlm.send_mail = self.catch_sendmail
+        self.sent_mail = []
+        self.ml.obey(dict)
+
+    def sender_matches(self, mail, sender):
+        pat = "(?P<address>" + sender + ")"
+        m = re.match(pat, mail["sender"], re.I)
+        if m:
+            return m.group("address")
+        else:
+            return None
+        
+    def replyto_matches(self, mail, replyto):
+        pat = "(.|\n)*(?P<address>" + replyto + ")"
+        m = re.match(pat, mail["text"], re.I)
+        if m:
+            return m.group("address")
+        else:
+            return None
+
+    def receiver_matches(self, mail, recipient):
+        return map(string.lower, mail["recipients"]) == [recipient.lower()]
+
+    def body_matches(self, mail, body):
+        if body:
+            pat = re.compile("(.|\n)*" + body + "(.|\n)*")
+            m = re.match(pat, mail["text"])
+            return m
+        else:
+            return 1
+
+    def headers_match(self, mail, header):
+        if header:
+            pat = re.compile("(.|\n)*" + header + "(.|\n)*", re.I)
+            m = re.match(pat, mail["text"])
+            return m
+        else:
+            return 1
+
+    def match(self, sender, replyto, receiver, body=None, header=None,
+              anti_header=None):
+        ret = None
+        for mail in self.sent_mail:
+            if replyto is None:
+                m1 = self.sender_matches(mail, sender)
+                m3 = self.receiver_matches(mail, receiver)
+                m4 = self.body_matches(mail, body)
+                m5 = self.headers_match(mail, header)
+                m6 = self.headers_match(mail, anti_header)
+                no_anti_header = anti_header == None or m6 == None
+                if m1 != None and m3 and m4 and m5 and no_anti_header:
+                    ret = m1
+                    self.sent_mail.remove(mail)
+                    break
+            else:
+                m1 = self.sender_matches(mail, sender)
+                m2 = self.replyto_matches(mail, replyto)
+                m3 = self.receiver_matches(mail, receiver)
+                m4 = self.body_matches(mail, body)
+                m5 = self.headers_match(mail, header)
+                m6 = self.headers_match(mail, anti_header)
+                no_anti_header = anti_header == None or m6 == None
+                if m1 != None and m2 != None and m3 and m4 and m5 and \
+                   no_anti_header:
+                    ret = m2
+                    self.sent_mail.remove(mail)
+                    break
+        self.failUnless(ret != None)
+        return ret
+
+    def no_more_mail(self):
+        self.failUnlessEqual(self.sent_mail, [])
+
+
+class SimpleCommandAddressTestCases(IncomingBase):
+
+    def testHelp(self):
+        self.send("outsider@example.com", "foo-help@example.com")
+        self.match("foo-ignore@example.com", None, "outsider@example.com", 
+                   "Subject: Help for")
+        self.no_more_mail()
+
+    def testOwner(self):
+        self.send("outsider@example.com", "foo-owner@example.com", "abcde")
+        self.match("outsider@example.com", None, "listmaster@example.com",
+                   "abcde")
+        self.no_more_mail()
+
+    def testIgnore(self):
+        self.send("outsider@example.com", "foo-ignore@example.com", "abcde")
+        self.no_more_mail()
+
+class OwnerCommandTestCases(IncomingBase):
+
+    def testList(self):
+        self.send("listmaster@example.com", "foo-list@example.com")
+        self.match("foo-ignore@example.com", None, "listmaster@example.com",
+                   "[uU][sS][eE][rR][12]@" +
+                        "[eE][xX][aA][mM][pP][lL][eE]\\.[cC][oO][mM]\n" +
+                   "[uU][sS][eE][rR][12]@" +
+                        "[eE][xX][aA][mM][pP][lL][eE]\\.[cC][oO][mM]\n")
+        self.no_more_mail()
+
+    def testListDenied(self):
+        self.send("outsider@example.com", "foo-list@example.com")
+        self.match("foo-ignore@example.com", None, "outsider@example.com", 
+                   "Subject: Subscriber list denied")
+        self.no_more_mail()
+
+    def testSetlist(self):
+        self.send("listmaster@example.com", "foo-setlist@example.com",
+                  "From: foo\n\nnew1@example.com\nuser1@example.com\n")
+        a = self.match("foo-ignore@example.com", 
+                       "foo-setlistyes-[^@]*@example.com", 
+                       "listmaster@example.com", 
+                       "Subject: Please moderate subscriber list")
+        self.no_more_mail()
+        
+        self.send("listmaster@example.com", a)
+        self.match("foo-ignore@example.com", None, "listmaster@example.com",
+                   "Subject: Subscriber list has been changed")
+        self.match("foo-ignore@example.com", None, "new1@example.com",
+                   "Subject: Welcome to")
+        self.match("foo-ignore@example.com", None, "user2@EXAMPLE.com",
+                   "Subject: Goodbye from")
+        self.no_more_mail()
+
+    def testSetlistSilently(self):
+        self.send("listmaster@example.com", "foo-setlistsilently@example.com",
+                  "From: foo\n\nnew1@example.com\nuser1@example.com\n")
+        a = self.match("foo-ignore@example.com", 
+                       "foo-setlistsilentyes-[^@]*@example.com", 
+                       "listmaster@example.com", 
+                       "Subject: Please moderate subscriber list")
+        self.no_more_mail()
+        
+        self.send("listmaster@example.com", a)
+        self.match("foo-ignore@example.com", None, "listmaster@example.com",
+                   "Subject: Subscriber list has been changed")
+        self.no_more_mail()
+
+    def testSetlistDenied(self):
+        self.send("outsider@example.com", "foo-setlist@example.com",
+                  "From: foo\n\nnew1@example.com\nnew2@example.com\n")
+        self.match("foo-ignore@example.com", 
+                   None,
+                   "outsider@example.com", 
+                   "Subject: You can't set the subscriber list")
+        self.no_more_mail()
+
+    def testSetlistBadlist(self):
+        self.send("listmaster@example.com", "foo-setlist@example.com",
+                  "From: foo\n\nBlah blah blah.\n")
+        self.match("foo-ignore@example.com", 
+                   None,
+                   "listmaster@example.com", 
+                   "Subject: Bad address list")
+        self.no_more_mail()
+
+    def testOwnerSubscribesSomeoneElse(self):
+        # Send subscription request. List sends confirmation request.
+        self.send("listmaster@example.com",
+                  "foo-subscribe-outsider=example.com@example.com")
+        a = self.match("foo-ignore@example.com", 
+                       "foo-subyes-[^@]*@example.com", 
+                       "listmaster@example.com",
+                       "Please confirm subscription")
+        self.no_more_mail()
+        
+        # Confirm sub. req. List sends welcome.
+        self.send("listmaster@example.com", a)
+        self.match("foo-ignore@example.com", 
+                   None, 
+                   "outsider@example.com", 
+                   "Welcome to the")
+        self.no_more_mail()
+
+    def testOwnerUnubscribesSomeoneElse(self):
+        # Send unsubscription request. List sends confirmation request.
+        self.send("listmaster@example.com",
+                  "foo-unsubscribe-outsider=example.com@example.com")
+        a = self.match("foo-ignore@example.com", 
+                       "foo-unsubyes-[^@]*@example.com", 
+                       "listmaster@example.com",
+                       "Subject: Please confirm UNsubscription")
+        self.no_more_mail()
+        
+        # Confirm sub. req. List sends welcome.
+        self.send("listmaster@example.com", a)
+        self.match("foo-ignore@example.com", None, "outsider@example.com", 
+                   "Goodbye")
+        self.no_more_mail()
+
+class SubscriptionTestCases(IncomingBase):
+
+    def confirm(self, recipient):
+        # List has sent confirmation request. Respond to it.
+        a = self.match("foo-ignore@example.com", 
+                       "foo-subyes-[^@]*@example.com", 
+                       recipient,
+                       "Please confirm subscription")
+        self.no_more_mail()
+        
+        # Confirm sub. req. List response will be analyzed later.
+        self.send("something.random@example.com", a)
+
+    def got_welcome(self, recipient):
+        self.match("foo-ignore@example.com", 
+                   None, 
+                   recipient, 
+                   "Welcome to the")
+        self.no_more_mail()
+
+    def approve(self, user_recipient):
+        self.match("foo-ignore@example.com", None, user_recipient)
+        a = self.match("foo-ignore@example.com", 
+                       "foo-subapprove-[^@]*@example.com",
+                       "listmaster@example.com")
+        self.send("listmaster@example.com", a)
+
+    def reject(self, user_recipient):
+        self.match("foo-ignore@example.com", None, user_recipient)
+        a = self.match("foo-ignore@example.com", 
+                       "foo-subreject-[^@]*@example.com",
+                       "listmaster@example.com")
+        self.send("listmaster@example.com", a)
+
+    def testSubscribeToUnmoderatedWithoutAddressNotOnList(self):
+        self.configure_list("free", "free")
+        self.send("outsider@example.com", "foo-subscribe@example.com")
+        self.confirm("outsider@example.com")
+        self.got_welcome("outsider@example.com")
+
+    def testSubscribeToUnmoderatedWithoutAddressAlreadyOnList(self):
+        self.configure_list("free", "free")
+        self.send("user1@example.com", "foo-subscribe@example.com")
+        self.confirm("user1@example.com")
+        self.got_welcome("user1@example.com")
+
+    def testSubscribeToUnmoderatedWithAddressNotOnList(self):
+        self.configure_list("free", "free")
+        self.send("somebody.else@example.com", 
+                  "foo-subscribe-outsider=example.com@example.com")
+        self.confirm("outsider@example.com")
+        self.got_welcome("outsider@example.com")
+
+    def testSubscribeToUnmoderatedWithAddressAlreadyOnList(self):
+        self.configure_list("free", "free")
+        self.send("somebody.else@example.com", 
+                  "foo-subscribe-user1=example.com@example.com")
+        self.confirm("user1@example.com")
+        self.got_welcome("user1@example.com")
+
+    def testSubscribeToModeratedWithoutAddressNotOnListApproved(self):
+        self.configure_list("moderated", "moderated")
+        self.send("outsider@example.com", "foo-subscribe@example.com")
+        self.confirm("outsider@example.com")
+        self.approve("outsider@example.com")
+        self.got_welcome("outsider@example.com")
+
+    def testSubscribeToModeratedWithoutAddressNotOnListRejected(self):
+        self.configure_list("moderated", "moderated")
+        self.send("outsider@example.com", "foo-subscribe@example.com")
+        self.confirm("outsider@example.com")
+        self.reject("outsider@example.com")
+
+    def testSubscribeToModeratedWithoutAddressAlreadyOnListApproved(self):
+        self.configure_list("moderated", "moderated")
+        self.send("user1@example.com", "foo-subscribe@example.com")
+        self.confirm("user1@example.com")
+        self.approve("user1@example.com")
+        self.got_welcome("user1@example.com")
+
+    def testSubscribeToModeratedWithoutAddressAlreadyOnListRejected(self):
+        self.configure_list("moderated", "moderated")
+        self.send("user1@example.com", "foo-subscribe@example.com")
+        self.confirm("user1@example.com")
+        self.reject("user1@example.com")
+
+    def testSubscribeToModeratedWithAddressNotOnListApproved(self):
+        self.configure_list("moderated", "moderated")
+        self.send("somebody.else@example.com", 
+                  "foo-subscribe-outsider=example.com@example.com")
+        self.confirm("outsider@example.com")
+        self.approve("outsider@example.com")
+        self.got_welcome("outsider@example.com")
+
+    def testSubscribeToModeratedWithAddressNotOnListRejected(self):
+        self.configure_list("moderated", "moderated")
+        self.send("somebody.else@example.com", 
+                  "foo-subscribe-outsider=example.com@example.com")
+        self.confirm("outsider@example.com")
+        self.reject("outsider@example.com")
+
+    def testSubscribeToModeratedWithAddressAlreadyOnListApproved(self):
+        self.configure_list("moderated", "moderated")
+        self.send("somebody.else@example.com", 
+                  "foo-subscribe-user1=example.com@example.com")
+        self.confirm("user1@example.com")
+        self.approve("user1@example.com")
+        self.got_welcome("user1@example.com")
+
+    def testSubscribeToModeratedWithAddressAlreadyOnListRejected(self):
+        self.configure_list("moderated", "moderated")
+        self.send("somebody.else@example.com", 
+                  "foo-subscribe-user1=example.com@example.com")
+        self.confirm("user1@example.com")
+        self.reject("user1@example.com")
+
+class UnsubscriptionTestCases(IncomingBase):
+
+    def confirm(self, recipient):
+        # List has sent confirmation request. Respond to it.
+        a = self.match("foo-ignore@example.com", 
+                       "foo-unsubyes-[^@]*@example.com", 
+                       recipient,
+                       "Please confirm UNsubscription")
+        self.no_more_mail()
+        
+        # Confirm sub. req. List response will be analyzed later.
+        self.send("something.random@example.com", a)
+
+    def got_goodbye(self, recipient):
+        self.match("foo-ignore@example.com", 
+                   None, 
+                   recipient, 
+                   "Goodbye from")
+        self.no_more_mail()
+
+    def testUnsubscribeWithoutAddressNotOnList(self):
+        self.send("outsider@example.com", "foo-unsubscribe@example.com")
+        self.confirm("outsider@example.com")
+        self.got_goodbye("outsider@example.com")
+
+    def testUnsubscribeWithoutAddressOnList(self):
+        self.send("user1@example.com", "foo-unsubscribe@example.com")
+        self.confirm("user1@example.com")
+        self.got_goodbye("user1@example.com")
+
+    def testUnsubscribeWithAddressNotOnList(self):
+        self.send("somebody.else@example.com", 
+                  "foo-unsubscribe-outsider=example.com@example.com")
+        self.confirm("outsider@example.com")
+        self.got_goodbye("outsider@example.com")
+
+    def testUnsubscribeWithAddressOnList(self):
+        self.send("somebody.else@example.com", 
+                  "foo-unsubscribe-user1=example.com@example.com")
+        self.confirm("user1@example.com")
+        self.got_goodbye("user1@example.com")
+
+class PostTestCases(IncomingBase):
+
+    msg = u"Subject: something \u00c4\n\nhello, world\n".encode("utf8")
+
+    def approve(self, user_recipient):
+        self.match("foo-ignore@example.com", None, user_recipient)
+        a = self.match("foo-ignore@example.com", 
+                       "foo-approve-[^@]*@example.com",
+                       "listmaster@example.com")
+        self.send("listmaster@example.com", a)
+
+    def reject(self, user_recipient):
+        self.match("foo-ignore@example.com", None, user_recipient)
+        a = self.match("foo-ignore@example.com", 
+                       "foo-reject-[^@]*@example.com",
+                       "listmaster@example.com")
+        self.send("listmaster@example.com", a)
+
+    def check_headers_are_encoded(self):
+        ok_chars = "\t\r\n"
+        for code in range(32, 127):
+            ok_chars = ok_chars + chr(code)
+        for mail in self.sent_mail:
+            text = mail["text"]
+            self.failUnless("\n\n" in text)
+            headers = text.split("\n\n")[0]
+            for c in headers:
+                if c not in ok_chars: print headers
+                self.failUnless(c in ok_chars)
+
+    def check_mail_to_list(self):
+        self.check_headers_are_encoded()
+        self.match("foo-bounce-.*@example.com", None, "USER1@example.com",
+                   body="hello, world",
+                   header="X-Foo: FOO",
+                   anti_header="Received:")
+        self.match("foo-bounce-.*@example.com", None, "user2@EXAMPLE.com",
+                   body="hello, world",
+                   header="x-foo: foo",
+                   anti_header="Received:")
+        self.no_more_mail()
+
+    def check_that_moderation_box_is_empty(self):
+        ml = self.mlm.open_list("foo@example.com")
+        self.failUnlessEqual(os.listdir(ml.moderation_box.boxdir), [])
+
+    def testSubscriberPostsToUnmoderated(self):
+        self.configure_list("free", "free")
+        self.send("user1@example.com", "foo@example.com", 
+                  self.msg)
+        self.check_mail_to_list()
+
+    def testOutsiderPostsToUnmoderated(self):
+        self.configure_list("free", "free")
+        self.send("outsider@example.com", "foo@example.com", self.msg)
+        self.check_mail_to_list()
+
+    def testSubscriberPostToAutomoderated(self):
+        self.configure_list("free", "auto")
+        self.check_that_moderation_box_is_empty()
+        self.send("user1@example.com", "foo@example.com", self.msg)
+        self.check_mail_to_list()
+        self.check_that_moderation_box_is_empty()
+
+    def testOutsiderPostsToAutomoderatedRejected(self):
+        self.configure_list("free", "auto")
+        self.check_that_moderation_box_is_empty()
+        self.send("outsider@example.com", "foo@example.com", self.msg)
+        self.reject("outsider@example.com")
+        self.check_that_moderation_box_is_empty()
+
+    def testOutsiderPostsToAutomoderatedApproved(self):
+        self.configure_list("free", "auto")
+        self.check_that_moderation_box_is_empty()
+        self.send("outsider@example.com", "foo@example.com", self.msg)
+        self.approve("outsider@example.com")
+        self.check_mail_to_list()
+        self.check_that_moderation_box_is_empty()
+
+    def testSubscriberPostsToModeratedRejected(self):
+        self.configure_list("free", "moderated")
+        self.check_that_moderation_box_is_empty()
+        self.send("user1@example.com", "foo@example.com", self.msg)
+        self.reject("user1@example.com")
+        self.check_that_moderation_box_is_empty()
+
+    def testOutsiderPostsToMderatedApproved(self):
+        self.configure_list("free", "moderated")
+        self.check_that_moderation_box_is_empty()
+        self.send("outsider@example.com", "foo@example.com", self.msg)
+        self.approve("outsider@example.com")
+        self.check_mail_to_list()
+        self.check_that_moderation_box_is_empty()
+
+    def testSubscriberPostsWithRequestToBeModerated(self):
+        self.configure_list("free", "free")
+
+        self.check_that_moderation_box_is_empty()
+        self.send("user1@example.com", "foo@example.com", self.msg,
+                  force_moderation=1)
+        self.match("foo-ignore@example.com", 
+                   None, 
+                   "user1@example.com", 
+                   "Subject: Please wait")
+        a = self.match("foo-ignore@example.com", 
+                       "foo-approve-[^@]*@example.com", 
+                       "listmaster@example.com")
+        self.no_more_mail()
+
+        self.send("listmaster@example.com", a)
+        self.check_mail_to_list()
+        self.check_that_moderation_box_is_empty()
+
+    def testSubscriberPostsWithModerationOverride(self):
+        self.configure_list("moderated", "moderated")
+        self.send("user1@example.com", "foo@example.com", self.msg,
+                  force_posting=1)
+        self.check_mail_to_list()
+        self.check_that_moderation_box_is_empty()
+
+class BounceTestCases(IncomingBase):
+
+    def check_subscriber_status(self, must_be):
+        ml = self.mlm.open_list("foo@example.com")
+        for id in ml.subscribers.groups():
+            self.failUnlessEqual(ml.subscribers.get(id, "status"), must_be)
+
+    def bounce_sent_mail(self):
+        for m in self.sent_mail[:]:
+            self.send("something@example.com", m["sender"], "eek")
+            self.failUnlessEqual(len(self.sent_mail), 0)
+
+    def send_mail_to_list_then_bounce_everything(self):
+        self.send("user@example.com", "foo@example.com", "hello")
+        for m in self.sent_mail[:]:
+            self.send("foo@example.com", m["sender"], "eek")
+            self.failUnlessEqual(len(self.sent_mail), 0)
+
+    def testBounceOnceThenRecover(self):
+        self.check_subscriber_status("ok")
+        self.send_mail_to_list_then_bounce_everything()
+
+        self.check_subscriber_status("bounced")
+        
+        ml = self.mlm.open_list("foo@example.com")
+        for id in ml.subscribers.groups():
+            bounce_id = ml.subscribers.get(id, "bounce-id")
+            self.failUnless(bounce_id)
+            self.failUnless(ml.bounce_box.has(bounce_id))
+
+        bounce_ids = []
+        now = time.time()
+        ml = self.mlm.open_list("foo@example.com")
+        ml.subscribers.lock()
+        for id in ml.subscribers.groups():
+            timestamp = float(ml.subscribers.get(id, "timestamp-bounced"))
+            self.failUnless(abs(timestamp - now) < 10.0)
+            ml.subscribers.set(id, "timestamp-bounced", "69.0")
+            bounce_ids.append(ml.subscribers.get(id, "bounce-id"))
+        ml.subscribers.save()
+
+        self.mlm.cleaning_woman(no_op)
+        self.check_subscriber_status("probed")
+
+        for bounce_id in bounce_ids:
+            self.failUnless(ml.bounce_box.has(bounce_id))
+
+        self.mlm.cleaning_woman(no_op)
+        ml = self.mlm.open_list("foo@example.com")
+        self.failUnlessEqual(len(ml.subscribers.groups()), 2)
+        self.check_subscriber_status("ok")
+        for bounce_id in bounce_ids:
+            self.failUnless(not ml.bounce_box.has(bounce_id))
+
+    def testBounceProbeAlso(self):
+        self.check_subscriber_status("ok")
+        self.send_mail_to_list_then_bounce_everything()
+        self.check_subscriber_status("bounced")
+        
+        ml = self.mlm.open_list("foo@example.com")
+        for id in ml.subscribers.groups():
+            bounce_id = ml.subscribers.get(id, "bounce-id")
+            self.failUnless(bounce_id)
+            self.failUnless(ml.bounce_box.has(bounce_id))
+
+        bounce_ids = []
+        now = time.time()
+        ml = self.mlm.open_list("foo@example.com")
+        ml.subscribers.lock()
+        for id in ml.subscribers.groups():
+            timestamp = float(ml.subscribers.get(id, "timestamp-bounced"))
+            self.failUnless(abs(timestamp - now) < 10.0)
+            ml.subscribers.set(id, "timestamp-bounced", "69.0")
+            bounce_ids.append(ml.subscribers.get(id, "bounce-id"))
+        ml.subscribers.save()
+
+        self.sent_mail = []
+        self.mlm.cleaning_woman(self.catch_sendmail)
+        self.check_subscriber_status("probed")
+        for bounce_id in bounce_ids:
+            self.failUnless(ml.bounce_box.has(bounce_id))
+        self.bounce_sent_mail()
+        self.check_subscriber_status("probebounced")
+
+        self.mlm.cleaning_woman(no_op)
+        ml = self.mlm.open_list("foo@example.com")
+        self.failUnlessEqual(len(ml.subscribers.groups()), 0)
+        for bounce_id in bounce_ids:
+            self.failUnless(not ml.bounce_box.has(bounce_id))
+
+    def testCleaningWomanJoinsAndBounceSplitsGroups(self):
+        # Check that each group contains one address and set the creation
+        # timestamp to an ancient time.
+        ml = self.mlm.open_list("foo@example.com")
+        bouncedir = os.path.join(ml.dirname, "bounce-box")
+        ml.subscribers.lock()
+        for id in ml.subscribers.groups():
+            addrs = ml.subscribers.in_group(id)
+            self.failUnlessEqual(len(addrs), 1)
+            bounce_id = ml.subscribers.get(id, "bounce-id")
+            self.failUnlessEqual(bounce_id, "..notexist..")
+            bounce_id = "bounce-" + id
+            ml.subscribers.set(id, "bounce-id", bounce_id)
+            bounce_path = os.path.join(bouncedir, bounce_id)
+            self.failUnless(not os.path.isfile(bounce_path))
+            f = open(bounce_path, "w")
+            f.close()
+            f = open(bounce_path + ".address", "w")
+            f.close()
+            self.failUnless(os.path.isfile(bounce_path))
+            ml.subscribers.set(id, "timestamp-created", "1")
+        ml.subscribers.save()
+
+        # Check that --cleaning-woman joins the two groups into one.
+        self.failUnlessEqual(len(ml.subscribers.groups()), 2)
+        self.mlm.cleaning_woman(no_op)
+        ml = self.mlm.open_list("foo@example.com")
+        self.failUnlessEqual(len(ml.subscribers.groups()), 1)
+        self.failUnlessEqual(os.listdir(bouncedir), [])
+        
+        # Check that a bounce splits the single group.
+        self.send_mail_to_list_then_bounce_everything()
+        ml = self.mlm.open_list("foo@example.com")
+        self.failUnlessEqual(len(ml.subscribers.groups()), 2)
+        
+        # Check that a --cleaning-woman immediately after doesn't join.
+        # (The groups are new, thus shouldn't be joined for a week.)
+        self.failUnlessEqual(len(ml.subscribers.groups()), 2)
+        self.mlm.cleaning_woman(no_op)
+        ml = self.mlm.open_list("foo@example.com")
+        self.failUnlessEqual(len(ml.subscribers.groups()), 2)
diff --git a/fix-config b/fix-config
new file mode 100755 (executable)
index 0000000..7ca6524
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+sed 's:^TEMPLATE_DIRS *=.*:TEMPLATE_DIRS = ["'$1'"]:' |
+sed 's:^DOTDIR *=.*:DOTDIR = os.path.expanduser("~/.enemies-of-carlotta"):'
diff --git a/qmqp.py b/qmqp.py
new file mode 100644 (file)
index 0000000..bc5d194
--- /dev/null
+++ b/qmqp.py
@@ -0,0 +1,137 @@
+#!/usr/bin/python
+
+# This module that implements sending mail via QMQP. See
+#
+#      http://cr.yp.to/proto/qmqp.html
+#
+# for a description of the protocol.
+#
+# This module was written by Jaakko Niemi <liiwi@lonesom.pp.fi> for
+# Enemies of Carlotta. It is licensed the same way as Enemies of Carlotta:
+# GPL version 2.
+
+import socket, string
+
+class QMQPException(Exception):
+    '''Base class for all exceptions raised by this module.'''
+
+class QMQPTemporaryError(QMQPException):
+    '''Class for temporary errors'''
+    def __init__(self, msg):
+        self.msg = msg
+        
+    def __str__(self):
+        return "QMQP-Server said: %s" % self.msg
+        
+class QMQPPermanentError(QMQPException):
+    '''Class for permanent errors'''
+    def __init__(self, msg):
+        self.msg = msg
+        
+    def __str__(self):
+        return "QMQP-Server said: %s" % self.msg
+
+class QMQPConnectionError(QMQPException):
+    '''Class for connection errors'''
+    def __init__(self, msg):
+        self.msg = msg
+
+    def __str__(self):
+        return "Error was: %s" % self.msg
+
+class QMQP:
+    '''I handle qmqp connection to a server'''
+
+    file = None
+
+    def __init__(self, host = 'localhost'):
+        '''Start'''
+        if host:
+            resp = self.connect(host)
+
+    def encode(self, stringi):
+           ret = '%d:%s,' % (len(stringi), stringi) 
+           return ret
+
+    def decode(self, stringi):
+           stringi = string.split(stringi, ':', 1)
+           stringi[1] = string.rstrip(stringi[1], ',')
+           if len(stringi[1]) is not int(stringi[0]):
+                   print 'malformed netstring encounterd'
+           return stringi[1]
+
+    def connect(self, host = 'localhost'):
+        for sres in socket.getaddrinfo(host, 628, 0, socket.SOCK_STREAM):
+            af, socktype, proto, canonname, sa = sres
+            try:
+                self.sock = socket.socket(af, socktype, proto)
+                self.sock.connect(sa)
+            except socket.error:
+                print 'connect failed'
+                if self.sock:
+                    self.sock.close()
+                self.sock = None
+                continue
+            break
+        if not self.sock:
+            raise socket.error
+        return 0
+        
+    def send(self, stringi):
+        if self.sock:
+            try:
+                self.sock.sendall(stringi)
+            except socket.error, err:
+                self.sock.close()
+                raise QMQPConnectionError, err
+        else:
+            print 'not connected'
+
+    def getreply(self):
+        if self.file is None:
+            self.file = self.sock.makefile('rb')
+        while 1:
+            line = self.file.readline()
+            if line == '':
+                self.sock.close()
+                print 'ERORORR'
+           break
+        line = self.decode(line)
+        return line
+
+    def quit(self):
+        self.sock.close()
+
+    def sendmail(self, from_addr, to_addrs, msg):
+           recipients = ''
+           msg = self.encode(msg)
+           from_addr = self.encode(from_addr)
+
+# I don't understand why len(to_addrs) <= 1 needs to be handled differently.
+# Anyway, it doesn't seem to work with Postfix. --liw
+#          if len(to_addrs) > 1:
+#                  for t in to_addrs:
+#                          recipients = recipients + self.encode(t)
+#          else:
+#                  recipients = self.encode(to_addrs[0])
+
+           for t in to_addrs:
+                   recipients = recipients + self.encode(t)
+           output = self.encode(msg + from_addr + recipients)
+           self.send(output)
+           ret = self.getreply()
+           if ret[0] == 'K':
+                   return ret[1:]
+           if ret[0] == 'Z':
+                   raise QMQPTemporaryError, ret[1:]
+           if ret[0] == 'D':
+                   raise QMQPPermanentError, ret[1:]
+
+if __name__ == '__main__':
+    a = QMQP()
+    maili = 'asfasdfsfdasfasd'
+    envelope_sender = 'liw@liw.iki.fi'
+    recips = [ 'liw@liw.iki.fi' ]    
+    retcode = a.sendmail(envelope_sender, recips, maili)
+    print retcode
+    a.quit()
diff --git a/templates/bounce-goodbye b/templates/bounce-goodbye
new file mode 100644 (file)
index 0000000..5d5f7cd
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Your mail bounces, goodbye from %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+You have been a subscriber to the %(list)s mailing list.
+
+Unfortunately, mail sent to you by the mailing list manager has bounced
+so much that you have been automatically removed.  Once your mail problems
+are solved, feel free to re-subscribe.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
diff --git a/templates/bounce-goodbye.es b/templates/bounce-goodbye.es
new file mode 100644 (file)
index 0000000..59245fd
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Su mensaje rebota, adiós desde %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Hasta ahora ha sido suscriptor de la lista  %(list)s.
+
+Desafortunadamente, ha rebotado tanto un mensaje que le envió el
+administrador de la lista que ha sido desuscrito de forma automática.
+Una vez corrija sus problemas de correo, será bienvenido a resuscribirse.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+
+Gracias.
diff --git a/templates/bounce-goodbye.fi b/templates/bounce-goodbye.fi
new file mode 100644 (file)
index 0000000..9c707a8
--- /dev/null
@@ -0,0 +1,16 @@
+From: %(From)s
+To: %(To)s
+Subject: Postisi ei toimi, näkemiin listalta %(list)s
+Content-type: text/plain; charset=utf-8
+
+Olet ollut tilaajana %(list)s -listalla.
+
+Valitettavasti sinulle lähetetyt sähköpostit aiheuttavat virheilmoituksia
+siinä määrin, että sinut on nyt automaattisesti poistettu listalta. Kunhan
+sähköpostiongelmasi on korjattu, olet tervetullut takaisin tilaajaksi.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
diff --git a/templates/bounce-goodbye.fr b/templates/bounce-goodbye.fr
new file mode 100644 (file)
index 0000000..a6f09e6
--- /dev/null
@@ -0,0 +1,23 @@
+From: %(From)s
+To: %(To)s
+Subject: Votre courriel ne peut être délivré, vous êtes désabonné de la liste %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour, 
+
+Vous vous étiez abonné à la liste de diffusion %(list)s.
+
+Malheureusement, les courriels qui vous ont été envoyés par le gestionnaire 
+de la liste de diffusion ne sont jamais arrivés à destination. Ce phénomène
+s'étant reproduit un grand nombre de fois, vous avez été automatiquement
+désabonné de la liste. Une fois que vos problèmes de courriels seront 
+corrigés, n'hésitez pas à vous réabonner.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse : 
+%(local)s-owner@%(domain)s .
+
+Merci.
diff --git a/templates/bounce-goodbye.sv b/templates/bounce-goodbye.sv
new file mode 100644 (file)
index 0000000..b2195d7
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Prenumerationen på %(list)s avbruten p.g.a. för många studsar
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Du har prenumererat på epostlistan %(list)s.
+
+Tyvärr har post skickad till dig av epostlistehanteraren studsat
+så mycket att du har blivit automatiskt borttagen. När problemen
+är lösta är du välkommen att prenumerera på nytt.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/bounce-owner-notification b/templates/bounce-owner-notification
new file mode 100644 (file)
index 0000000..441efc3
--- /dev/null
@@ -0,0 +1,35 @@
+From: %(From)s
+To: %(To)s
+Subject: Address removed from %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the 
+
+    %(list)s
+
+mailing list to the human owners of the list.
+
+The following address has been removed from the list due to bouncing:
+
+    %(address)s
+
+The bounce message is attached at the bottom of this mail. Unless there is
+something really strange going on, this mail is only for your information
+and you need take no action.
+
+Thanks.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-owner-notification.es b/templates/bounce-owner-notification.es
new file mode 100644 (file)
index 0000000..3827372
--- /dev/null
@@ -0,0 +1,36 @@
+From: %(From)s
+To: %(To)s
+Subject: Dirección eliminada de %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+El gestor que opera la lista de correo 
+
+    %(list)s
+
+
+ha enviado este mensaje a los operadores humanos de la lista.
+
+La siguiente dirección ha sido eliminada de la lista debido a rebotes:
+
+    %(address)s
+
+Al final de este mensaje se adjunta el mensaje que rebotó. A menos que
+esté pasando algo realmente extraño, este mensaje es sólo informativo, y
+no necesita tomar ninguna acción al respecto.
+
+Gracias.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-owner-notification.fi b/templates/bounce-owner-notification.fi
new file mode 100644 (file)
index 0000000..ef959d1
--- /dev/null
@@ -0,0 +1,30 @@
+From: %(From)s
+To: %(To)s
+Subject: Osoite poistettu listalta %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Tämän meilin on lähettänyt postituslistaohjelmisto, joka hoitaa
+listaa
+
+    %(list)s
+
+listan omistajille. Osoite 
+
+    %(address)s
+
+on poistettu listalta toimimattomuuden takia. Virheilmoitus on tämän
+viestin lopussa. Tämä meili on vain tiedoksi, jos mitään erityisen
+omituista ei ole tapahtumassa.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-owner-notification.fr b/templates/bounce-owner-notification.fr
new file mode 100644 (file)
index 0000000..80a911a
--- /dev/null
@@ -0,0 +1,36 @@
+From: %(From)s
+To: %(To)s
+Subject: Adresse supprimée de la liste %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour, 
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+
+    %(list)s
+
+aux personnes qui sont responsables de la liste.
+
+L'adresse suivante a été enlevée de la liste en raison de problème dans
+la livraison du courriel destiné à :
+
+    %(address)s
+
+Le message qui n'a pu être délivré se trouve en bas de ce courriel. À 
+moins qu'il n'y ait quelque chose de vraiment particulier, ce courriel n'est
+présent qu'à titre informatif; vous n'avez pas à vous en souciez.
+
+Merci.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-owner-notification.sv b/templates/bounce-owner-notification.sv
new file mode 100644 (file)
index 0000000..754d3b1
--- /dev/null
@@ -0,0 +1,35 @@
+From: %(From)s
+To: %(To)s
+Subject: Adress borttagen från %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter listan
+
+    %(list)s
+
+till listans ägare.
+
+Följande adress har tagits bort från listan p.g.a. att posten studsar:
+
+    %(address)s
+
+Studsmeddelandet finns i slutet av det här brevet. Såvida inte något
+riktigt märkligt pågår är det här brevet bara till för att informera 
+dig och du behöver inte vidta någon åtgärd.
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-warning b/templates/bounce-warning
new file mode 100644 (file)
index 0000000..dc98901
--- /dev/null
@@ -0,0 +1,37 @@
+From: %(From)s
+To: %(To)s
+Subject: Your mail is bouncing, %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+You are subscribed to the %(list)s mailig list.
+
+Mail sent to you by the mailing list software has bounced at least once
+in the past week or so. If this was a temporary error, you can ignore
+this warning. However, if your mail continues to bounce, you will
+eventually be automatically unsubscribed from the list. Sorry about
+the inconvenience.
+
+The first bounce message is attached.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-warning.es b/templates/bounce-warning.es
new file mode 100644 (file)
index 0000000..c39c34c
--- /dev/null
@@ -0,0 +1,34 @@
+From: %(From)s
+To: %(To)s
+Subject: Sus mensajes están rebotando, %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Usted es suscriptor de la lista de correo %(list)s.
+
+Al menos uno de los mensajes que le envió la lista durante la última
+semana rebotó. Si fue un problema temporal, puede ignorar este aviso.
+Sin embargo, si su correo continúa rebotando, acabará siendo desuscrito
+de forma automática de esta lista. Sentimos el inconveniente.
+
+Más abajo encontrará el primer mensaje que rebotó.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-warning.fi b/templates/bounce-warning.fi
new file mode 100644 (file)
index 0000000..83a18f1
--- /dev/null
@@ -0,0 +1,32 @@
+From: %(From)s
+To: %(To)s
+Subject: Postisi aiheuttaa virheilmoituksia, %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Olet tilaajana listalla %(list)s .
+
+Sinulle lähetetty sähköposti on aiheuttanut ainakin yhden virheilmoituksen
+viimeisen viikon aikana. Jos tämä oli tilapäinen ongelma, voit unohtaa
+koko asian. Jos virheet kuitenkin jatkuvat, sinut poistetaan lopulta
+listalta. Valitamme.
+
+Ensimmäinen virheilmoitus on tämän viestin lopussa.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-warning.fr b/templates/bounce-warning.fr
new file mode 100644 (file)
index 0000000..2b3bb91
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Subject: Votre courriel ne peut être délivré, %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour, 
+
+Vous êtes abonné à la liste de diffusion %(list)s .
+
+Les courriels qui vous ont été envoyés par le gestionnaire
+de la liste de diffusion ne sont jamais arrivés à destination. 
+Ce phénomène s'est produit au moins une fois depuis la semaine dernière.
+S'il s'agit d'une erreur temporaire, vous pouvez ignorer ce message 
+d'avertissement. Cependant, si les échecs se reproduisent, il se peut
+que vous soyez automatiquement désabonné de la liste de diffusion. Nous
+en sommes désolé.
+
+Le premier message qui n'a pu être délivré est cité ci-dessous.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/bounce-warning.sv b/templates/bounce-warning.sv
new file mode 100644 (file)
index 0000000..c16f111
--- /dev/null
@@ -0,0 +1,37 @@
+From: %(From)s
+To: %(To)s
+Subject: Din epost studsar, %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Du prenumererar på epostlistan %(list)s.
+
+Epost skickad till dig av epostlistehanteraren har studsat åtminstone
+en gång under den senaste veckan eller så. Om detta berodde på ett
+tillfälligt fel kan du bortse från den här varningen. Fortsätter dina
+brev att studsa kommer du i slutändan automatiskt att bli borttagen 
+från listan över prenumeranter. Vi beklagar detta.
+
+Den första studsen återfinns nedan.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=bounce.txt
+
+%(bounce)s--%(boundary)s--
+
diff --git a/templates/footer b/templates/footer
new file mode 100644 (file)
index 0000000..697ef92
--- /dev/null
@@ -0,0 +1,3 @@
+
+-- 
+To unsubscribe, send mail to %(local)s-unsubscribe@%(domain)s.
diff --git a/templates/footer.es b/templates/footer.es
new file mode 100644 (file)
index 0000000..99458e5
--- /dev/null
@@ -0,0 +1,3 @@
+
+-- 
+Para desuscribirse, mande un mensaje a %(local)s-unsubscribe@%(domain)s
diff --git a/templates/footer.fi b/templates/footer.fi
new file mode 100644 (file)
index 0000000..f88f90f
--- /dev/null
@@ -0,0 +1,3 @@
+
+-- 
+Poistu listalta:  %(local)s-unsubscribe@%(domain)s .
diff --git a/templates/footer.fr b/templates/footer.fr
new file mode 100644 (file)
index 0000000..43b584e
--- /dev/null
@@ -0,0 +1,3 @@
+
+-- 
+Pour ne plus recevoir de message : mailto:%(local)s-unsubscribe@%(domain)s
diff --git a/templates/footer.sv b/templates/footer.sv
new file mode 100644 (file)
index 0000000..e3f3557
--- /dev/null
@@ -0,0 +1,4 @@
+
+-- 
+Om du vill avsluta prenumerationen, skicka ett brev till
+%(local)s-unsubscribe@%(domain)s.
diff --git a/templates/help b/templates/help
new file mode 100644 (file)
index 0000000..9ebe603
--- /dev/null
@@ -0,0 +1,38 @@
+From: %(From)s
+To: %(To)s
+Subject: Help for the %(list)s mailing list
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+this is the help text for the %(list)s mailing list.
+
+The list is operated by the EoC mailing list manager. It understands
+the following command addresses:
+
+    %(local)s-help@%(domain)s
+
+       Sends this help text.
+
+    %(local)s-subscribe@%(domain)s
+    
+       Subscribe to the list. You will get a confirmation request.
+       
+    %(local)s-subscribe-foo=bar@%(domain)s
+    
+       Subscribe the address foo@bar to the list. foo@bar will get the
+       confirmation request.
+
+    %(local)s-unsubscribe@%(domain)s
+    
+       Unsubscribe from the list. You will get a confirmation request.
+       
+    %(local)s-unsubscribe-foo=bar@%(domain)s
+    
+       Unsubscribe the address foo@bar from the list. foo@bar will get the
+       confirmation request.
+
+If you have problems that are not solved by this help text, please contact
+the human owner of the list at %(local)s-owner@%(domain)s.
+
+Thank you.
diff --git a/templates/help.es b/templates/help.es
new file mode 100644 (file)
index 0000000..3836db1
--- /dev/null
@@ -0,0 +1,39 @@
+From: %(From)s
+To: %(To)s
+Subject: Ayuda de la lista de correo %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+éste es el texto de ayuda de la lista de correo  %(list)s.
+
+La lista la lleva el gestor de listas de correo EoC, que
+comprende las siguientes órdenes mediante direcciones de correo:
+
+    %(local)s-help@%(domain)s
+
+       Envía este texto de ayuda.
+
+    %(local)s-subscribe@%(domain)s
+    
+       Suscribirse a la lista. Recibirá una petición de confirmación.
+       
+    %(local)s-subscribe-foo=bar@%(domain)s
+    
+       Suscribir la dirección foo@bar a la lista. foo@bar recibirá la
+       petición de confirmación.
+
+    %(local)s-unsubscribe@%(domain)s
+    
+       Desuscribirse de la lista. Recibirá una petición de confirmación.
+       
+    %(local)s-unsubscribe-foo=bar@%(domain)s
+    
+       Desuscribir la dirección foo@bar de la lista. foo@bar recibirá la
+       petición de confirmación.
+
+Si tiene algún problema que no resuelva este texto de ayuda, póngase en
+contacto por favor con la persona que supervisa la lista en
+%(local)s-owner@%(domain)s .
+
+Gracias.
diff --git a/templates/help.fi b/templates/help.fi
new file mode 100644 (file)
index 0000000..bb41161
--- /dev/null
@@ -0,0 +1,34 @@
+From: %(From)s
+To: %(To)s
+Subject: Ohjeita listalle %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tämä on postituslistan  %(list)s ohjeteksti.
+
+Listaa pyörittää postituslistaohjelmisto nimeltä EoC. Se ymmärtää
+seuraavat komento-osoitteet:
+
+    %(local)s-help@%(domain)s
+
+       Lähettää tämän ohjetekstin.
+
+    %(local)s-subscribe@%(domain)s
+    
+       Tilauspyyntö. Vastauksena tulee vahvistuspyyntö.
+       
+    %(local)s-subscribe-foo=bar@%(domain)s
+    
+       Tilauspyyntö siten, että listalle lisätään osoite foo@bar .
+       foo@bar saa myös vahvistuspyynnön.
+
+    %(local)s-unsubscribe@%(domain)s
+    
+       Listalta poistumispyyntö. Tämäkin pitää vahvistaa.
+       
+    %(local)s-unsubscribe-foo=bar@%(domain)s
+    
+       Pyyntö poistaa foo@bar listalta. foo@bar saa vahvistuspyynnön.
+
+Jos sinulla on ongelmia, jotka eivät ratkea tämän ohjetekstin avulla,
+voit kysyä apua listan omistajilta (jotka ovat ihmisiä) osoitteesta
+%(local)s-owner@%(domain)s.
diff --git a/templates/help.fr b/templates/help.fr
new file mode 100644 (file)
index 0000000..6731007
--- /dev/null
@@ -0,0 +1,41 @@
+From: %(From)s
+To: %(To)s
+Subject: Message d'aide de la liste de diffusion %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour, 
+
+Voici le message d'aide pour la liste de diffusion %(list)s.
+
+La liste fonctionne grâce au gestionnaire de liste de diffusion 
+« EoC ». Il est capable de traiter les commandes suivantes :
+
+    %(local)s-help@%(domain)s
+
+       Envoie ce message d'aide.
+
+    %(local)s-subscribe@%(domain)s
+    
+       Vous abonne à la liste. Vous recevrez une demande de 
+       confirmation.
+       
+    %(local)s-subscribe-foo=bar@%(domain)s
+    
+       Abonne l'adresse foo@bar à la liste de diffusion. foo@bar
+       recevra une demande de confirmation.
+
+    %(local)s-unsubscribe@%(domain)s
+    
+       Vous désabonne de la liste de diffusion. Vous recevrez une 
+       demande de confirmation.
+       
+    %(local)s-unsubscribe-foo=bar@%(domain)s
+    
+       Désabonne l'adresse foo@bar de la liste de diffusion. foo@bar
+       recevra une demande de confirmation.
+
+Si vous rencontrez des problèmes qui ne sont pas traités dans ce message, 
+veuillez prendre contact avec la personne responsable de la liste de 
+diffusion à l'adresse : %(local)s-owner@%(domain)s.
+
+Merci.
diff --git a/templates/help.sv b/templates/help.sv
new file mode 100644 (file)
index 0000000..0a1a84c
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Subject: Hjälp för epostlistan %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Detta är hjälptexten till epostlistan %(list)s.
+
+Listan sköts av epostlistehanteraren EoC. Den förstår följande
+kommandoadresser:
+
+    %(local)s-help@%(domain)s
+
+       Skickar den här hjälptexten.
+
+    %(local)s-subscribe@%(domain)s
+    
+       Prenumerera på listan. Du kommer att få ett meddelande som 
+       ber dig bekräfta att du vill prenumerera.
+       
+    %(local)s-subscribe-foo=bar@%(domain)s
+    
+       Anmäl adressen foo@bar som prenumerant. foo@bar kommer att
+       få en bekräftelseförfrågan.
+
+    %(local)s-unsubscribe@%(domain)s
+    
+       Säg upp prenumerationen på listan. Du kommer att bli ombedd
+       att bekräfta.
+       
+    %(local)s-unsubscribe-foo=bar@%(domain)s
+    
+       Säg upp prenumerationen för foo@bar. foo@bar kommer att få
+       bekräfta.
+
+Om du har problem som inte den här hjälptexten kan lösa, kontakta 
+listans ägare på adressen %(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/list b/templates/list
new file mode 100644 (file)
index 0000000..242b9d6
--- /dev/null
@@ -0,0 +1,10 @@
+From: %(From)s
+To: %(To)s
+Subject: Subscribers for %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Subscribers for the %(list)s list, as requested.
+
+%(addresses)s
+
+Total: %(count)s addresses.
diff --git a/templates/list-sorry b/templates/list-sorry
new file mode 100644 (file)
index 0000000..a57f88e
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Subscriber list denied for %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Sorry, you are not the list owner for %(list)s.
diff --git a/templates/list-sorry.es b/templates/list-sorry.es
new file mode 100644 (file)
index 0000000..483bd4f
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Lista de suscriptores de %(list)s denegada
+Content-type: text/plain; charset=utf-8
+
+Lo siento, no está en la lista de dueños de %(list)s .
diff --git a/templates/list-sorry.fi b/templates/list-sorry.fi
new file mode 100644 (file)
index 0000000..445495a
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Et saa tilaajaluetteloa listalle %(list)s
+Content-type: text/plain; charset=utf-8
+
+Valitan, et ole listan %(list)s omistaja.
diff --git a/templates/list-sorry.fr b/templates/list-sorry.fr
new file mode 100644 (file)
index 0000000..73b106f
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Impossible d'obtenir la liste des abonnés à %(list)s
+Content-type: text/plain; charset=utf-8
+
+Désolé, vous n'êtes pas le responsable de la liste %(list)s.
diff --git a/templates/list-sorry.sv b/templates/list-sorry.sv
new file mode 100644 (file)
index 0000000..b23e9ad
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Begäran om prenumerantlista för %(list)s nekad
+Content-type: text/plain; charset=iso-8859-1
+
+Tyvärr, du är inte ägaren till listan %(list)s.
diff --git a/templates/list.es b/templates/list.es
new file mode 100644 (file)
index 0000000..de5e4e7
--- /dev/null
@@ -0,0 +1,10 @@
+From: %(From)s
+To: %(To)s
+Subject: Suscriptores de la lista %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tal como me pidió, estos son los suscriptores de la lista %(list)s .
+
+%(addresses)s
+
+Total: %(count)s direcciones.
diff --git a/templates/list.fi b/templates/list.fi
new file mode 100644 (file)
index 0000000..39c24e5
--- /dev/null
@@ -0,0 +1,10 @@
+From: %(From)s
+To: %(To)s
+Subject: Tilaajaluettelo: %(list)s
+Content-type: text/plain; charset=utf-8
+
+Pyynnöstä luettelen listan %(list)s tilaajat.
+
+%(addresses)s
+
+Yhteensä %(count)s osoitetta.
diff --git a/templates/list.fr b/templates/list.fr
new file mode 100644 (file)
index 0000000..7a553bd
--- /dev/null
@@ -0,0 +1,11 @@
+From: %(From)s
+To: %(To)s
+Subject: Liste des abonnés pour la liste %(list)s
+Content-type: text/plain; charset=utf-8
+
+Voici la liste des abonnés à la liste de diffusion 
+%(list)s
+
+%(addresses)s
+
+Total: %(count)s adresses.
diff --git a/templates/list.sv b/templates/list.sv
new file mode 100644 (file)
index 0000000..c40a896
--- /dev/null
@@ -0,0 +1,10 @@
+From: %(From)s
+To: %(To)s
+Subject: Prenumeranter på %(list)s
+Content-type: text/plain; charset=utf-8
+
+Här är listan över prenumeranter på listan %(list)s, enligt begäran.
+
+%(addresses)s
+
+Totalt: %(count)s adresser.
diff --git a/templates/msg-moderate b/templates/msg-moderate
new file mode 100644 (file)
index 0000000..f188f05
--- /dev/null
@@ -0,0 +1,37 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Please moderate message to %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the 
+
+    %(list)s
+
+mailing list to the human moderators of the list.
+
+Should the message at the bottom be allowed to be sent to the list?
+If so, reply to this mail or send mail to
+
+    %(confirm)s
+
+If not, send mail to
+
+    %(deny)s
+
+Thanks.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/msg-moderate.es b/templates/msg-moderate.es
new file mode 100644 (file)
index 0000000..303befd
--- /dev/null
@@ -0,0 +1,37 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Por favor, modere este mensaje enviado a %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje lo ha enviado el gestor de listas de correo que lleva
+
+    %(list)s
+
+a los humanos que la supervisan.
+
+¿Debería permitirse enviar a la lista el mensaje que sigue? En caso
+afirmativo, responda a este mensaje o envíe un mensaje a
+
+    %(confirm)s
+
+Si no, envíe un mensaje a
+
+    %(deny)s
+
+Gracias.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/msg-moderate.fi b/templates/msg-moderate.fi
new file mode 100644 (file)
index 0000000..462d16f
--- /dev/null
@@ -0,0 +1,35 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Hyväksy viesti listalle %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa
+
+    %(list)s
+
+käsittelevä ohjelmisto listan omistajille.
+
+Pitäisikö alla oleva viesti hyväksyä ja lähettää kaikille tilaajille?
+Jos pitäisi, vastaa tähän viestiin tai lähetä viesti osoitteeseen
+
+    %(confirm)s
+
+Jos ei pitäisi, lähetä viesti osoitteeseen
+
+    %(deny)s
+
+Kiitos.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/msg-moderate.fr b/templates/msg-moderate.fr
new file mode 100644 (file)
index 0000000..ae77cd0
--- /dev/null
@@ -0,0 +1,39 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Veuillez modérer le message pour %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+
+    %(list)s
+
+aux personnes qui sont responsables de la liste.
+    
+
+Le message qui ce trouve à la fin doit-il être envoyé à la liste 
+de diffusion ? 
+Si oui, répondez à ce message ou envoyez un courriel à 
+
+    %(confirm)s
+
+Si non, envoyez un courriel à 
+
+    %(deny)s
+
+Merci.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/msg-moderate.sv b/templates/msg-moderate.sv
new file mode 100644 (file)
index 0000000..268fd80
--- /dev/null
@@ -0,0 +1,37 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Var god moderera meddelande till %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter listan
+
+    %(list)s
+
+till listans ägare.
+
+Ska meddelandet som visas nedan få skickas till listan?
+I så fall, svara på det här brevet eller skicka ett tomt meddelande till
+
+    %(confirm)s
+
+Om inte, skicka ett meddelande till
+
+    %(deny)s
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/msg-wait b/templates/msg-wait
new file mode 100644 (file)
index 0000000..12d41b0
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Please wait for moderation to %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+Your message to the list has been sent to the moderators for approval.
+This might take a while. Please be patient.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
diff --git a/templates/msg-wait.es b/templates/msg-wait.es
new file mode 100644 (file)
index 0000000..99d6993
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Por favor, aguarde a la moderación de %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje se lo ha enviado el gestor de listas de correo que opera
+sobre %(list)s .
+
+He enviado a los moderadores el mensaje que envió a la lista para que
+lo aprueben. Esto puede tardar un poco. Por favor, sea paciente.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+
+Gracias.
diff --git a/templates/msg-wait.fi b/templates/msg-wait.fi
new file mode 100644 (file)
index 0000000..6fa2b8f
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Odota omistajan vahvistusta: %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Listalle lähettämäsi viesti on lähetetty listan omistajille hyväksyntää
+varten. Tämä voi kestää hetken.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
+
diff --git a/templates/msg-wait.fr b/templates/msg-wait.fr
new file mode 100644 (file)
index 0000000..2050a0f
--- /dev/null
@@ -0,0 +1,22 @@
+From: %(From)s
+To: %(To)s
+Subject: Veuillez attendre la modération pour votre envoi sur %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+%(list)s .
+
+Votre message à destination de la liste a été envoyé aux modérateurs 
+pour qu'ils l'approuvent. Cela peut prendre un certain temps. Soyez
+patient.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
diff --git a/templates/msg-wait.sv b/templates/msg-wait.sv
new file mode 100644 (file)
index 0000000..6c028da
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Var god vänta på moderering av din postning till %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter epostlistan
+%(list)s.
+
+Ditt meddelande till listan har skickats till moderatorerna för god-
+kännande. Det kan ta ett litet tag. Ha tålamod!
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/setlist-badlist b/templates/setlist-badlist
new file mode 100644 (file)
index 0000000..1cb161c
--- /dev/null
@@ -0,0 +1,33 @@
+From: %(From)s
+To: %(To)s
+Subject: Bad address list for %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+The address list you sent was syntactically incorrect. You need to have
+exactly one address per line, and nothing else.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-badlist.es b/templates/setlist-badlist.es
new file mode 100644 (file)
index 0000000..314d182
--- /dev/null
@@ -0,0 +1,33 @@
+From: %(From)s
+To: %(To)s
+Subject: Dirección incorrecta para %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje se lo ha enviado el gestor de listas de correo que opera
+sobre %(list)s .
+
+La sintaxis de la dirección de lista a la que ha enviado el mensaje es
+incorrecta. Debe haber una única dirección por línea, y nada más.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+
+Gracias.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-badlist.fi b/templates/setlist-badlist.fi
new file mode 100644 (file)
index 0000000..d4843ab
--- /dev/null
@@ -0,0 +1,29 @@
+From: %(From)s
+To: %(To)s
+Subject: Virheelinen tilaajalista listalle %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Lähettämäsi osoiteluettelo oli muodollisesti epäpätevä. Luettelossa
+pitää olla täsmälleen yksi osoite per rivi, eikä muuta.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-badlist.fr b/templates/setlist-badlist.fr
new file mode 100644 (file)
index 0000000..010a6ba
--- /dev/null
@@ -0,0 +1,35 @@
+From: %(From)s
+To: %(To)s
+Subject: Mauvaise adresse de liste pour %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+%(list)s .
+
+L'adresse à laquelle vous avez envoyé ce message contenait une erreur de
+syntaxe. Vous devez expressément avoir une adresse par ligne, et rien 
+d'autre.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-badlist.sv b/templates/setlist-badlist.sv
new file mode 100644 (file)
index 0000000..36c2a99
--- /dev/null
@@ -0,0 +1,33 @@
+From: %(From)s
+To: %(To)s
+Subject: Felaktig adresslista för %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter listan
+%(list)s.
+
+Adresslistan du skickade är syntaktiskt felaktig. Du måste skriva exakt
+en adress per rad och inget annat.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-confirm b/templates/setlist-confirm
new file mode 100644 (file)
index 0000000..c2577de
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Please moderate subscriber list for %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+One of the owners of the list (or someone pretending to be one) has asked
+for the subscriber list for the list to be changed.  The request mail,
+with the new set of describers, is attached.  If you accept the
+change, please respond to this mail. Otherwise please just ignore it.
+
+Note that the whole subscriber list will be changed to the list of
+addresses below. All old subscriptions will be forgotten, unless they
+are also on the new list of subscribers.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-confirm.es b/templates/setlist-confirm.es
new file mode 100644 (file)
index 0000000..55a280f
--- /dev/null
@@ -0,0 +1,41 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Por favor, modere la lista de suscriptores de %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje se lo ha enviado el gestor de listas de correo que opera
+sobre %(list)s .
+
+Uno de los miembros de la lista (o alguien que pretende serlo) ha pedido
+que se cambie el registro de suscriptores. Se incluye más adelante el
+mensaje de la petición, con el nuevo conjunto de suscriptores. Si acepta
+el cambio, responda a este mensaje. En caso contrario, limítese a
+ignorarlo.
+
+Tenga en cuenta que se cambiará la lista de suscriptores completa por la
+lista que aparece debajo. Olvidaré todos los suscriptores antiguos a menos
+que también estén en la lista nueva.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+
+Gracias.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-confirm.fi b/templates/setlist-confirm.fi
new file mode 100644 (file)
index 0000000..4332714
--- /dev/null
@@ -0,0 +1,34 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Varmista tilaajaluettelon muutos listalle %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain: charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Listan omistaja (tai sellaisena esiintyvä) on pyytänyt tilaajalistaa
+vaihdettavaksi allaolevan meilin mukaiseksi. Jos hyväksyt tämän
+muutokset, vastaa tähän meiliin. Muuten jätä se huomiotta.
+
+Huomaa, että koko tilaajalista vaihtuu alla olevan listan mukaiseksi.
+Listalla jo olevat osoitteet unohdetaan, jos niitä ei ole alla.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-confirm.fr b/templates/setlist-confirm.fr
new file mode 100644 (file)
index 0000000..fc35071
--- /dev/null
@@ -0,0 +1,42 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Veuillez modérer la liste des abonnés pour %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+%(list)s .
+
+L'un des propriétaires de la liste (ou quelqu'un se faisant passer pour
+l'un d'entres eux) a demandé à ce que la liste des abonnés de cette
+liste soit modifiée. Le courriel en question, avec la nouvelle liste des
+abonnés se trouve ci-dessous. Si vous acceptez les changements, veuillez
+répondre à ce courriel. Dans le cas contraire, ignorez-le.
+
+Veuillez noter que la totalité des abonnés à la liste sera remplacée
+par la liste des adresses ci-dessous. Tous les anciens abonnements
+seront supprimés, à moins qu'il ne se trouve dans la nouvelle liste.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-confirm.sv b/templates/setlist-confirm.sv
new file mode 100644 (file)
index 0000000..4ce558c
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Var god kontrollera ny prenumerantlista för %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter epostlistan
+%(list)s.
+
+En av listägarna (eller någon som låtsas vara en) har begärt att
+prenumerantlistan ändras. Brevet innehållande begäran återfinns nedan.
+Om du accepterar ändringen, svara på det här brevet. I annat fall, 
+ignorera det bara.
+
+Observera att hela prenumerantlistan kommer att ändras till nedanstående
+adresslista. Alla tidigare prenumeranter som inte finns med på den nya 
+listan kommer att glömmas.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/setlist-done b/templates/setlist-done
new file mode 100644 (file)
index 0000000..19c9d5e
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Subscriber list has been changed for %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+For your information: On request from a list owner, the subscriber list
+for the list has been replaced with a new one.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
diff --git a/templates/setlist-done.es b/templates/setlist-done.es
new file mode 100644 (file)
index 0000000..8b97596
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Se ha cambiado la lista de suscriptores de %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje se lo ha enviado el gestor de listas de correo que opera
+sobre %(list)s .
+
+Para su información: Bajo petición de uno de los gestores de la lista,
+se ha cambiado la lista de suscriptores por una nueva.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+
+Gracias.
diff --git a/templates/setlist-done.fi b/templates/setlist-done.fi
new file mode 100644 (file)
index 0000000..8109d7f
--- /dev/null
@@ -0,0 +1,16 @@
+From: %(From)s
+To: %(To)s
+Subject: Tilaajalista vaihdettu listalle %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Tiedoksi: Listan (erään) omistajan pyynnöstä tilaajalista on vaihdettu
+uuteen.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
diff --git a/templates/setlist-done.fr b/templates/setlist-done.fr
new file mode 100644 (file)
index 0000000..a6591a0
--- /dev/null
@@ -0,0 +1,22 @@
+From: %(From)s
+To: %(To)s
+Subject: La liste des abonnés de la liste %(list)s a changé
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+%(list)s .
+
+Pour votre information : à la demande d'un des propriétaires de la 
+liste, la liste des abonnés pour la liste a été remplacée par une 
+nouvelle.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
diff --git a/templates/setlist-done.sv b/templates/setlist-done.sv
new file mode 100644 (file)
index 0000000..d30cd7d
--- /dev/null
@@ -0,0 +1,20 @@
+From: %(From)s
+To: %(To)s
+Subject: Prenumerantlista har ändrats för %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter listan
+%(list)s.
+
+För din kännedom: På begäran från en listägare har listan med 
+prenumeranter ersatts med en ny.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/setlist-sorry b/templates/setlist-sorry
new file mode 100644 (file)
index 0000000..7172adb
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: You can't set the subscriber list for %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Sorry, you are not the list owner for %(list)s.
diff --git a/templates/setlist-sorry.es b/templates/setlist-sorry.es
new file mode 100644 (file)
index 0000000..a665cb9
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: No puede cambiar la lista de suscriptores de %(list)s
+Content-type: text/plain; charset=utf-8
+
+Lo siento, no está en la lista de gestores de %(list)s.
diff --git a/templates/setlist-sorry.fi b/templates/setlist-sorry.fi
new file mode 100644 (file)
index 0000000..8bbd9d3
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Et voi vaihtaa tilaajalistaa listalle %(list)s
+Content-type: text/plain; charset=utf-8
+
+Et ole listan %(list)s omistaja.
diff --git a/templates/setlist-sorry.fr b/templates/setlist-sorry.fr
new file mode 100644 (file)
index 0000000..d7df206
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Vous ne pouvez pas définir la liste des abonnés pour %(list)s
+Content-type: text/plain; charset=utf-8
+
+Désolé, mais vous n'êtes pas l'un des propriétaires de la liste %(list)s.
diff --git a/templates/setlist-sorry.sv b/templates/setlist-sorry.sv
new file mode 100644 (file)
index 0000000..4217b23
--- /dev/null
@@ -0,0 +1,6 @@
+From: %(From)s
+To: %(To)s
+Subject: Du kan inte ställa in prenumerantlistan för %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tyvärr, du är inte någon av ägarna till %(list)s.
diff --git a/templates/sub-confirm b/templates/sub-confirm
new file mode 100644 (file)
index 0000000..c306afe
--- /dev/null
@@ -0,0 +1,44 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Please confirm subscription to %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+Someone has asked you to be added as a subscriber to the list. See the
+mail they sent at the bottom. If you did not send it yourself, please
+contact the sender or their administrator and ask what is going on.
+
+If you reply to this mail, you confirm that you wish to be added as a
+subscriber. The contents of the reply doesn't matter. Usually, you can
+just use the normal reply feature in your mail program. Alternatively,
+you can send mail to the address below:
+
+    %(confirm)s
+
+If you don't want to subscribe, just ignore this mail.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-confirm.es b/templates/sub-confirm.es
new file mode 100644 (file)
index 0000000..18a11f2
--- /dev/null
@@ -0,0 +1,43 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Por favor, confirme su suscripción a %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje se lo ha enviado el gestor automático que opera la
+lista de correo %(list)s .
+
+Alguien ha pedido que usted sea añadido como suscriptor de la lista.
+Al final verá el mensaje enviado. Si no lo envió usted mismo, por favor,
+póngase en contacto con quien lo hizo o con su administrador, para saber
+qué sucede.
+
+Si responde a este mensaje, confirmará que desea ser añadido como
+suscriptor. No importa el contenido de la respuesta. Por lo común,
+basta con usar la función normal de respuesta de su programa de correo.
+De forma alternativa, puede enviar un mensaje a la siguiente dirección:
+
+    %(confirm)s
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+Gracias.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-confirm.fi b/templates/sub-confirm.fi
new file mode 100644 (file)
index 0000000..1e6f57f
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Vahvista tilauspyyntö listalle %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Joku on pyytänyt, että sinut lisätään listan tilaajaksi. Pyyntö on
+tämän viestin lopussa. Jos et lähettänyt sitä itse, ole hyvä ja kysy
+lähettäjältä tai heidän ylläpidoltaan mistä on kyse.
+
+Jos vastaat tähän viestiin, vahvistat, että haluat tilata listan.
+Vastausviestisi sisällöllä ei ole väliä. Vastaamisen pitäisi onnistua
+sähköpostiohjelmasi normaalilla vastaustoiminnolla. Vaihtoehtoisesti
+voit lähettää viestin tähän osoitteeseen:
+
+    %(confirm)s
+
+Jos et halua tilata listaa, sinun ei tarvitse tehdä mitään.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-confirm.fr b/templates/sub-confirm.fr
new file mode 100644 (file)
index 0000000..3d1f2b3
--- /dev/null
@@ -0,0 +1,48 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Veuillez confirmer votre abonnement à %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+de la liste de diffusion %(list)s .
+
+Quelqu'un vient de demander de vous abonner à cette liste de diffusion.
+Regardez le message qui se trouve à la fin. Si vous n'avez pas envoyez
+vous-même directement ce message, veuillez prendre contact avec 
+l'expéditeur de ce message ou bien avec l'administrateur pour demander
+des explications.
+
+En répondant à ce courriel, vous confirmerez que vous souhaitez vous
+abonner à cette liste. Le contenu du message n'a pas d'importance. 
+Typiquement, vous pouvez utiliser la fonction répondre de votre 
+logiciel de lecture des courriers électroniques. Vous pouvez également
+envoyer un courriel à l'adresse ci-dessous :
+
+    %(confirm)s
+
+Si vous ne souhaitez pas vous abonner, ignorez simplement ce courriel.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s 
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-confirm.sv b/templates/sub-confirm.sv
new file mode 100644 (file)
index 0000000..f48266f
--- /dev/null
@@ -0,0 +1,45 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Bekräfta prenumeration på %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter epostlistan
+%(list)s.
+
+Du, eller någon i ditt ställe, har begärt att du läggs till som 
+prenumerant på listan. Meddelandet i fråga finns längst ner. Om du inte
+skickade det själv, kontakta avsändaren eller dennes administratör och
+fråga vad som pågår.
+
+Genom att besvara det här brevet bekräftar du att du vill vara med som
+prenumerant på listan. Innehållet i svaret spelar ingen roll. Vanligtvis
+går det bra att använda den vanliga svara-funktionen i ditt epostprogram.
+Alternativt kan du skicka ett tomt meddelande till följande adress:
+
+    %(confirm)s
+
+Om du inte vill prenumerera, strunta bara i det här brevet.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-moderate b/templates/sub-moderate
new file mode 100644 (file)
index 0000000..6fc83d3
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Please moderate subscription to %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the 
+
+    %(list)s
+
+mailing list to the human owners of the list.
+
+Should the following address be allowed to subscribe to the list?
+
+    %(subscriber)s
+
+If so, reply to this mail or send mail to
+
+    %(confirm)s
+
+If you wish to reject the subscriber, send mail to
+
+    %(deny)s
+
+Thanks.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-moderate.es b/templates/sub-moderate.es
new file mode 100644 (file)
index 0000000..ef2d45a
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Por favor, modere esta suscripción a %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje lo ha enviado el gestor que opera la lista de correo
+
+    %(list)s
+
+a sus administradores humanos.
+
+¿Debería permitirse a la siguiente dirección suscribirse a la lista?
+
+    %(subscriber)s
+
+En caso afirmativo, responda a este mensaje o envíe uno a
+
+    %(confirm)s
+
+Si desea rechazar al suscriptor, envíe un mensaje a
+
+    %(deny)s
+
+Gracias.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-moderate.fi b/templates/sub-moderate.fi
new file mode 100644 (file)
index 0000000..6e3aaf0
--- /dev/null
@@ -0,0 +1,36 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Käsittele tilauspyyntö: %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa
+
+    %(list)s
+
+käsittelevä ohjelmisto listan omistajille.
+
+Pitäisikö seuraava osoite lisätä tilaajalistalle?
+
+    %(subscriber)s
+
+Jos pitäisi, vastaa tähän viestiin tai lähetä viesti osoitteeseen
+
+    %(confirm)s
+
+Jos ei pitäisi, lähetä viesti osoitteeseen
+
+    %(deny)s
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-moderate.fr b/templates/sub-moderate.fr
new file mode 100644 (file)
index 0000000..3a8bcf7
--- /dev/null
@@ -0,0 +1,41 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Veuillez modérer l'abonnement à %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+
+    %(list)s
+
+aux personnes qui sont responsables de la liste.
+
+L'adresse suivante doit-elle être autorisée à s'abonner à la liste ?
+
+    %(subscriber)s
+
+Si oui, répondez à ce courriel ou envoyez un message à
+
+    %(confirm)s
+
+Si vous souhaitez rejeter l'abonnement, envoyez un message à
+
+    %(deny)s
+
+
+Merci.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-moderate.sv b/templates/sub-moderate.sv
new file mode 100644 (file)
index 0000000..ec2ce95
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Godkänn ny prenumeration på %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter listan
+
+    %(list)s
+
+till listans ägare.
+
+Ska följande adress få lov att prenumerera på listan?
+
+    %(subscriber)s
+
+I så fall, svara på det här brevet eller skicka ett tomt brev till
+
+    %(confirm)s
+
+Om du vill avvisa ansökan, skicka ett tomt brev till
+
+    %(deny)s
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/sub-owner-notification b/templates/sub-owner-notification
new file mode 100644 (file)
index 0000000..83453a0
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Address added to %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the 
+
+    %(list)s
+
+mailing list to the human owners of the list.
+
+The following address has been added to the list:
+
+    %(address)s
+
+Unless there is something really strange going on, this mail is only
+for your information and you need take no action.
+
+Thanks.
diff --git a/templates/sub-owner-notification.es b/templates/sub-owner-notification.es
new file mode 100644 (file)
index 0000000..701017e
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Dirección añadida a %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este es un mensaje del gestor de listas de correo que opera sobre
+
+    %(list)s
+
+a sus administradores humanos.
+
+Se ha añadido a la lista la siguiente dirección:
+
+    %(address)s
+
+A menos que pase algo realmente extraño, este mensaje es sólo
+informativo y no hace falta que tome ninguna acción al respecto.
+
+Gracias.
diff --git a/templates/sub-owner-notification.fi b/templates/sub-owner-notification.fi
new file mode 100644 (file)
index 0000000..bc0c0d0
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Tilaaja lisätty: %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa
+
+    %(list)s
+
+käsittelevä ohjelmisto listan omistajille.
+
+Seuraava osoite on lisätty listalle:
+
+    %(address)s
+
+Tämä tiedoksi teille. Teidän ei tarvitse tehdä mitään asialle, paitsi jos
+jotain oikein omituista on tapahtunut.
diff --git a/templates/sub-owner-notification.fr b/templates/sub-owner-notification.fr
new file mode 100644 (file)
index 0000000..548d827
--- /dev/null
@@ -0,0 +1,22 @@
+From: %(From)s
+To: %(To)s
+Subject: Adresse ajoutée à %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour, 
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+
+    %(list)s
+
+aux personnes qui sont responsables de la liste.
+
+L'adresse suivante a été ajoutée à la liste :
+
+    %(address)s
+
+À moins que quelque chose de particulier vous semble suspect, ce message
+vous est uniquement envoyé à titre informatif et ne nécessite pas 
+traitement particulier.
+
+Merci.
diff --git a/templates/sub-owner-notification.sv b/templates/sub-owner-notification.sv
new file mode 100644 (file)
index 0000000..83453a0
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Address added to %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the 
+
+    %(list)s
+
+mailing list to the human owners of the list.
+
+The following address has been added to the list:
+
+    %(address)s
+
+Unless there is something really strange going on, this mail is only
+for your information and you need take no action.
+
+Thanks.
diff --git a/templates/sub-reject b/templates/sub-reject
new file mode 100644 (file)
index 0000000..ffd1915
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Subscription denied to %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+You tried to subscribe to the list, but the moderator has rejected
+your subscription. Sorry.
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
diff --git a/templates/sub-reject.es b/templates/sub-reject.es
new file mode 100644 (file)
index 0000000..873eca5
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Suscripción a %(list)s rechazada
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje se lo ha enviado el gestor de listas de correo que opera
+sobre %(list)s .
+
+Ha intentado suscribirse a la lista, pero el moderador rechazó la
+suscripción. Lo siento.
+
+Si tiene problemas, haga el favor de ponerse en contacto con los
+administradores humanos de la lista en %(local)s-owner@%(domain)s .
+
+Gracias.
diff --git a/templates/sub-reject.fi b/templates/sub-reject.fi
new file mode 100644 (file)
index 0000000..6cf271e
--- /dev/null
@@ -0,0 +1,12 @@
+From: %(From)s
+To: %(To)s
+Subject: Tilaus evätty: %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Yritit tilata listan, mutta listan omistajat eväsivät pyyntösi.
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
diff --git a/templates/sub-reject.fr b/templates/sub-reject.fr
new file mode 100644 (file)
index 0000000..548fbd4
--- /dev/null
@@ -0,0 +1,18 @@
+From: %(From)s
+To: %(To)s
+Subject: Rejet de l'abonnement à %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel vous a été envoyé par le gestionnaire de la liste de 
+diffusion %(list)s .
+    
+Vous avez demandez à vous abonner à la liste, mais le modérateur a
+rejet votre demande d'abonnement. Désolé.
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
diff --git a/templates/sub-reject.sv b/templates/sub-reject.sv
new file mode 100644 (file)
index 0000000..a7a99ec
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Prenumeration på %(list)s avvisad
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter epostlistan
+%(list)s.
+
+Du försökte prenumerera på listan, men en moderator har avslagit din
+begäran. Beklagar.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/sub-wait b/templates/sub-wait
new file mode 100644 (file)
index 0000000..1ef973d
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Please wait for moderation to %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+You have confirmed that you want to subscribe to the list. List
+subscription is moderated, however, and the list owner needs to manually
+process your request. This might take a while. Please be patient.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
diff --git a/templates/sub-wait.es b/templates/sub-wait.es
new file mode 100644 (file)
index 0000000..28e300f
--- /dev/null
@@ -0,0 +1,22 @@
+From: %(From)s
+To: %(To)s
+Subject: Por favor, espere por la moderación de %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje se lo ha enviado el gestor de listas de correo que opera
+sobre %(list)s .
+
+Ha confirmado que desea suscribirse a la lista. La suscripción está
+moderada, sin embargo, y hace falta que el administrador de la lista
+procese manualmente la petición. Esto puede llevar un tiempo. Por favor,
+sea paciente.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s .
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s .
+
+Gracias.
diff --git a/templates/sub-wait.fi b/templates/sub-wait.fi
new file mode 100644 (file)
index 0000000..f53673c
--- /dev/null
@@ -0,0 +1,16 @@
+From: %(From)s
+To: %(To)s
+Subject: Odota omistajan vahvistusta: %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Olet vahvistanut, että haluat tilata listan. Listan tilaus on rajoitettua
+ja listan omistajien tarvitsee vielä käsitellä tilauspyyntösi käsin.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
diff --git a/templates/sub-wait.fr b/templates/sub-wait.fr
new file mode 100644 (file)
index 0000000..619584c
--- /dev/null
@@ -0,0 +1,23 @@
+From: %(From)s
+To: %(To)s
+Subject: Veuillez attendre la modération pour %(list)s 
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+%(list)s .
+
+Vous avez confirmé que vous souhaitiez vous abonner à la liste. 
+L'inscription à la liste est malgré tout modérée, et le propriétaire
+de la liste devra manuellement accepter votre requête. Cela peut prendre
+un certain temps. Soyez patient.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
diff --git a/templates/sub-wait.sv b/templates/sub-wait.sv
new file mode 100644 (file)
index 0000000..6a95f7e
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Var god vänta på godkännande av prenumerationen på %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter epostlistan
+%(list)s.
+
+Du har bekräftat att du vill prenumerera på listan. Prenumerationen är
+dock inte öppen för alla, och därför måste listans ägare manuellt 
+godkänna din begäran. Det kan ta en stund, så ha tålamod!
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/sub-welcome b/templates/sub-welcome
new file mode 100644 (file)
index 0000000..3e18880
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Welcome to %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Welcome to the %(list)s mailing list.
+
+If you wish to unsubscribe, send mail to 
+%(local)s-unsubscribe@%(domain)s .
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
diff --git a/templates/sub-welcome.es b/templates/sub-welcome.es
new file mode 100644 (file)
index 0000000..0a8e13e
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Bienvenido a %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bienvenido de la lista de c orreo %(list)s .
+
+Si desea cancelar la suscripción, envíe un mensaje a
+%(local)s-unsubscribe@%(domain)s .
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s.
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s.
+
+Gracias.
diff --git a/templates/sub-welcome.fi b/templates/sub-welcome.fi
new file mode 100644 (file)
index 0000000..b32c978
--- /dev/null
@@ -0,0 +1,15 @@
+From: %(From)s
+To: %(To)s
+Subject: Tervetuloa listalle %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tervetuloa postituslistalle %(list)s .
+
+Jos haluat poistua listalta, lähetä postia osoitteeseen
+%(local)s-unsubscribe@%(domain)s .
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
diff --git a/templates/sub-welcome.fr b/templates/sub-welcome.fr
new file mode 100644 (file)
index 0000000..818275a
--- /dev/null
@@ -0,0 +1,18 @@
+From: %(From)s
+To: %(To)s
+Subject: Bienvenue sur %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bienvenue sur la liste de diffusion %(list)s .
+
+Si vous souhaitez vous désabonner, envoyez un courriel à 
+%(local)s-unsubscribe@%(domain)s.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
diff --git a/templates/sub-welcome.sv b/templates/sub-welcome.sv
new file mode 100644 (file)
index 0000000..0b848b9
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Välkommen till %(list)s
+Content-type: text/plain; charset=utf-8
+
+Välkommen till epostlistan %(list)s!
+
+Om du vill avsluta prenumerationen, skicka ett tomt brev till
+%(local)s-unsubscribe@%(domain)s.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/unsub-confirm b/templates/unsub-confirm
new file mode 100644 (file)
index 0000000..0187b3a
--- /dev/null
@@ -0,0 +1,43 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Please confirm UNsubscription to %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the
+%(list)s mailing list.
+
+Someone has asked you to be removed as a subscriber from the list. See
+the mail they sent at the bottom. If you did not send it yourself,
+please contact the sender or their administrator and ask what is going on.
+
+If you reply to this mail, you confirm that you wish to be removed as a
+subscriber. Usually, you can just use the normal reply feature in your
+mail program. Alternatively, you can send mail to the address below:
+
+    %(confirm)s
+
+You don't need to do anything to continue your subscription.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/unsub-confirm.es b/templates/unsub-confirm.es
new file mode 100644 (file)
index 0000000..52d596b
--- /dev/null
@@ -0,0 +1,45 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Por favor, confirme la cancelación de suscripción a %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje lo ha enviado el gestor de listas de correo que opera
+sobre %(list)s .
+
+Alguien ha pedido que se cancele su suscripción a la lista. Lea el mensaje
+que se envió al final del todo. Si no lo envió usted mismo, por favor,
+póngase en contacto con quien lo hizo, o sus administradores, y pregúnteles
+qué está pasando.
+
+Si responde a este mensaje, confirmará que desea la cancelación. Por lo
+común, basta con usar la función normal de respuesta de su programa de
+correo. De forma alternativa, puede enviar mensajes a las siguientes
+direcciones:
+
+    %(confirm)s
+
+Si desea continuar con su suscripción, no hace falta que haga nada.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s.
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s.
+
+Gracias.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/unsub-confirm.fi b/templates/unsub-confirm.fi
new file mode 100644 (file)
index 0000000..f886b84
--- /dev/null
@@ -0,0 +1,40 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Vahvista poistumispyyntö: %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa %(list)s
+operoiva ohjelmisto.
+
+Joku on pyytänyt, että sinut poistetaan listalta. Pyyntö on tämän viestin
+lopussa. Jos et lähettänyt sitä itse, ole hyvä ja kysy lähettäjältä tai
+heidän ylläpidoltaan mistä on kyse.
+
+Jos vastaat tähän viestiin, vahvistat, että haluat poistua listalta.
+Vastausviestisi sisällöllä ei ole väliä. Vastaamisen pitäisi onnistua
+sähköpostiohjelmasi normaalilla vastaustoiminnolla. Vaihtoehtoisesti
+voit lähettää viestin tähän osoitteeseen:
+
+    %(confirm)s
+
+Jos et halua tilata listaa, sinun ei tarvitse tehdä mitään.
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/unsub-confirm.fr b/templates/unsub-confirm.fr
new file mode 100644 (file)
index 0000000..4615e07
--- /dev/null
@@ -0,0 +1,46 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Veuillez confirmer votre DÉsabonnement à %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour,
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+%(list)s .
+
+Quelqu'un a demandé à ce que vous soyez désabonné de la liste. Regardez
+le message qui ce trouve en dessous. Si vous n'avez pas envoyé ce message
+vous-même, veuillez prendre contact avec l'expéditeur ou son 
+administrateur et demandez des explications.
+
+En répondant à ce courriel, vous confirmerez que vous souhaitez vous
+désabonner. Généralement, il suffit simplement d'utiliser la fonction 
+répondre de votre logiciel de lecture des courriers électroniques. Vous
+pouvez également envoyez un courriel à l'adresse indiquée ci-dessous :
+
+    %(confirm)s
+
+Si vous souhaitez toujours être abonné, aucune action n'est nécessaire.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/unsub-confirm.sv b/templates/unsub-confirm.sv
new file mode 100644 (file)
index 0000000..873b289
--- /dev/null
@@ -0,0 +1,45 @@
+From: %(From)s
+To: %(To)s
+Reply-To: %(confirm)s
+Subject: Bekräfta avslut på prenumeration på %(list)s
+Content-type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+
+This is a multipart message in MIME format
+
+--%(boundary)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter epostlistan
+%(list)s.
+
+Du, eller någon i ditt ställe, har begärt att din prenumeration på 
+listan ska avslutas. Meddelandet i fråga finns längst ner. Om du inte 
+skickade det själv, kontakta avsändaren eller dennes administratör och 
+fråga vad som pågår.
+
+Genom att besvara det här brevet bekräftar du att du inte längre vill
+prenumerera på listan. Innehållet i svaret spelar ingen roll. Vanligtvis
+går det bra att använda den vanliga svara-funktionen i ditt epostprogram.
+Alternativt kan du skicka ett tomt meddelande till följande adress:
+
+    %(confirm)s
+
+Om du vill fortsätta prenumerera behöver du inte göra någonting alls.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till to %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
+
+--%(boundary)s
+Content-type: message/rfc822
+Content-disposition: inline; filename=original.txt
+
+%(origmail)s--%(boundary)s--
+
diff --git a/templates/unsub-goodbye b/templates/unsub-goodbye
new file mode 100644 (file)
index 0000000..9eb4b6b
--- /dev/null
@@ -0,0 +1,14 @@
+From: %(From)s
+To: %(To)s
+Subject: Goodbye from %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Goodbye from the %(list)s mailing list.
+
+For instructions on using the mailing list manager software, send
+mail to %(local)s-help@%(domain)s .
+
+If you have problems, please contact the human owners of the list
+at %(local)s-owner@%(domain)s .
+
+Thank you.
diff --git a/templates/unsub-goodbye.es b/templates/unsub-goodbye.es
new file mode 100644 (file)
index 0000000..309d6af
--- /dev/null
@@ -0,0 +1,14 @@
+From: %(From)s
+To: %(To)s
+Subject: Despedida de %(list)s
+Content-type: text/plain; charset=utf-8
+
+Nos despedimos de usted desde la lista de correo %(list)s.
+
+Si desea instrucciones sobre el uso del software gestor de listas, envíe
+un mensaje a %(local)s-help@%(domain)s.
+
+Si tiene problemas, por favor, póngase en contacto con las personas que
+gestionan la lista en %(local)s-owner@%(domain)s.
+
+Gracias.
diff --git a/templates/unsub-goodbye.fi b/templates/unsub-goodbye.fi
new file mode 100644 (file)
index 0000000..af1d710
--- /dev/null
@@ -0,0 +1,12 @@
+From: %(From)s
+To: %(To)s
+Subject: Näkemiin listalta %(list)s
+Content-type: text/plain; charset=utf-8
+
+Näkemiin postituslistalta %(list)s .
+
+Postituslistaohjelmiston käyttöohjeet saa pyytämällä osoitteesta
+%(local)s-help@%(domain)s .
+
+Listan omistajat saat kiinni osoitteesta
+%(local)s-owner@%(domain)s .
diff --git a/templates/unsub-goodbye.fr b/templates/unsub-goodbye.fr
new file mode 100644 (file)
index 0000000..7de36e9
--- /dev/null
@@ -0,0 +1,15 @@
+From: %(From)s
+To: %(To)s
+Subject: Confirmation de votre désabonnement de %(list)s
+Content-type: text/plain; charset=utf-8
+
+Votre désabonnement de la liste de diffusion %(list)s est confirmé.
+
+Pour obtenir des informations sur l'utilisation du logiciel de gestion de
+la liste de diffusion, envoyez un courriel à %(local)s-help@%(domain)s .
+
+Si vous rencontrez des problèmes, veuillez prendre contact avec la
+personne responsable de la liste de diffusion à l'adresse :
+%(local)s-owner@%(domain)s .
+
+Merci.
diff --git a/templates/unsub-goodbye.sv b/templates/unsub-goodbye.sv
new file mode 100644 (file)
index 0000000..91e3081
--- /dev/null
@@ -0,0 +1,14 @@
+From: %(From)s
+To: %(To)s
+Subject: Farväl från %(list)s
+Content-type: text/plain; charset=utf-8
+
+Din prenumeration på %(list)s har härmed avslutats.
+
+För instruktioner om hur man använder epostlistehanteraren, skicka
+ett brev till %(local)s-help@%(domain)s.
+
+Om du har problem, kontakta personerna som äger listan på
+%(local)s-owner@%(domain)s.
+
+Tack.
diff --git a/templates/unsub-owner-notification b/templates/unsub-owner-notification
new file mode 100644 (file)
index 0000000..e18ee36
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Address removed from %(list)s
+Content-type: text/plain; charset=us-ascii
+
+Hello, 
+
+This mail has been sent by the mailing list manager operating the 
+
+    %(list)s
+
+mailing list to the human owners of the list.
+
+The following address has unsubscribed itself from the list:
+
+    %(address)s
+
+Unless there is something really strange going on, this mail is only
+for your information and you need take no action.
+
+Thanks.
diff --git a/templates/unsub-owner-notification.es b/templates/unsub-owner-notification.es
new file mode 100644 (file)
index 0000000..97b01aa
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Dirección eliminada de %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hola,
+
+Este mensaje lo ha enviado el gestor de listas de correo que opera sobre
+
+    %(list)s
+
+a sus administradores humanos.
+
+Se ha cancelado la suscripción a la lista de la siguiente dirección:
+
+    %(address)s
+
+A menos que pase algo realmente extraño, este mensaje sólo es informativo
+y no necesita tomar ninguna acción al respecto.
+
+Gracias.
diff --git a/templates/unsub-owner-notification.fi b/templates/unsub-owner-notification.fi
new file mode 100644 (file)
index 0000000..a72ea23
--- /dev/null
@@ -0,0 +1,17 @@
+From: %(From)s
+To: %(To)s
+Subject: Osoite poistettu listalta %(list)s
+Content-type: text/plain; charset=utf-8
+
+Tämän viestin on lähettänyt postituslistaa
+
+    %(list)s
+
+käsittelevä ohjelmisto listan omistajille.
+
+Seuraava osoite on poistanut itsensä listalta:
+
+    %(address)s
+
+Tämä tiedoksi teille. Teidän ei tarvitse tehdä mitään asialle, paitsi jos
+jotain oikein omituista on tapahtunut.
diff --git a/templates/unsub-owner-notification.fr b/templates/unsub-owner-notification.fr
new file mode 100644 (file)
index 0000000..0272399
--- /dev/null
@@ -0,0 +1,22 @@
+From: %(From)s
+To: %(To)s
+Subject: Adresse supprimée sur %(list)s
+Content-type: text/plain; charset=utf-8
+
+Bonjour, 
+
+Ce courriel a été envoyé par le gestionnaire de la liste de diffusion
+
+    %(list)s
+
+aux personnes qui sont responsables de la liste.
+
+L'adresse suivante s'est désabonnée d'elle-même de la liste :
+
+    %(address)s
+
+À moins que quelque chose de particulier vous semble suspect, ce message
+vous est uniquement envoyé à titre informatif et ne nécessite pas
+traitement particulier.
+
+Merci.
diff --git a/templates/unsub-owner-notification.sv b/templates/unsub-owner-notification.sv
new file mode 100644 (file)
index 0000000..cfec27c
--- /dev/null
@@ -0,0 +1,21 @@
+From: %(From)s
+To: %(To)s
+Subject: Adress borttagen från %(list)s
+Content-type: text/plain; charset=utf-8
+
+Hej! 
+
+Det här brevet skickades av epostlistehanteraren som sköter listan
+
+    %(list)s
+
+till listans ägare.
+
+Följande adress har avslutat sin prenumeration på listan:
+
+    %(address)s
+
+Såvida inte något riktigt märkligt pågår är det här brevet bara till
+för att informera dig och du behöver inte vidta någon åtgärd.
+
+Tack.
\ No newline at end of file
diff --git a/testrun.py b/testrun.py
new file mode 100644 (file)
index 0000000..53c1dbd
--- /dev/null
@@ -0,0 +1,9 @@
+import unittest
+import os
+
+suite = unittest.TestSuite()
+for n in filter(lambda fn: fn[-8:] == "Tests.py", os.listdir(".")):
+    suite.addTest(unittest.defaultTestLoader.loadTestsFromName(n[:-3]))
+
+runner = unittest.TextTestRunner()
+runner.run(suite)