Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8438d08e8a |
@@ -1,8 +0,0 @@
|
|||||||
*.pyc
|
|
||||||
build/
|
|
||||||
test/*.out
|
|
||||||
test/*.tstout
|
|
||||||
test/*.log
|
|
||||||
test.db
|
|
||||||
dist
|
|
||||||
MANIFEST
|
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
Version 2, June 1991
|
|
||||||
|
|
||||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The licenses for most software are designed to take away your
|
|
||||||
freedom to share and change it. By contrast, the GNU General Public
|
|
||||||
License is intended to guarantee your freedom to share and change free
|
|
||||||
software--to make sure the software is free for all its users. This
|
|
||||||
General Public License applies to most of the Free Software
|
|
||||||
Foundation's software and to any other program whose authors commit to
|
|
||||||
using it. (Some other Free Software Foundation software is covered by
|
|
||||||
the GNU Lesser General Public License instead.) You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
this service if you wish), that you receive source code or can get it
|
|
||||||
if you want it, that you can change the software or use pieces of it
|
|
||||||
in new free programs; and that you know you can do these things.
|
|
||||||
|
|
||||||
To protect your rights, we need to make restrictions that forbid
|
|
||||||
anyone to deny you these rights or to ask you to surrender the rights.
|
|
||||||
These restrictions translate to certain responsibilities for you if you
|
|
||||||
distribute copies of the software, or if you modify it.
|
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
|
||||||
gratis or for a fee, you must give the recipients all the rights that
|
|
||||||
you have. You must make sure that they, too, receive or can get the
|
|
||||||
source code. And you must show them these terms so they know their
|
|
||||||
rights.
|
|
||||||
|
|
||||||
We protect your rights with two steps: (1) copyright the software, and
|
|
||||||
(2) offer you this license which gives you legal permission to copy,
|
|
||||||
distribute and/or modify the software.
|
|
||||||
|
|
||||||
Also, for each author's protection and ours, we want to make certain
|
|
||||||
that everyone understands that there is no warranty for this free
|
|
||||||
software. If the software is modified by someone else and passed on, we
|
|
||||||
want its recipients to know that what they have is not the original, so
|
|
||||||
that any problems introduced by others will not reflect on the original
|
|
||||||
authors' reputations.
|
|
||||||
|
|
||||||
Finally, any free program is threatened constantly by software
|
|
||||||
patents. We wish to avoid the danger that redistributors of a free
|
|
||||||
program will individually obtain patent licenses, in effect making the
|
|
||||||
program proprietary. To prevent this, we have made it clear that any
|
|
||||||
patent must be licensed for everyone's free use or not licensed at all.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
GNU GENERAL PUBLIC LICENSE
|
|
||||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
|
||||||
|
|
||||||
0. This License applies to any program or other work which contains
|
|
||||||
a notice placed by the copyright holder saying it may be distributed
|
|
||||||
under the terms of this General Public License. The "Program", below,
|
|
||||||
refers to any such program or work, and a "work based on the Program"
|
|
||||||
means either the Program or any derivative work under copyright law:
|
|
||||||
that is to say, a work containing the Program or a portion of it,
|
|
||||||
either verbatim or with modifications and/or translated into another
|
|
||||||
language. (Hereinafter, translation is included without limitation in
|
|
||||||
the term "modification".) Each licensee is addressed as "you".
|
|
||||||
|
|
||||||
Activities other than copying, distribution and modification are not
|
|
||||||
covered by this License; they are outside its scope. The act of
|
|
||||||
running the Program is not restricted, and the output from the Program
|
|
||||||
is covered only if its contents constitute a work based on the
|
|
||||||
Program (independent of having been made by running the Program).
|
|
||||||
Whether that is true depends on what the Program does.
|
|
||||||
|
|
||||||
1. You may copy and distribute verbatim copies of the Program's
|
|
||||||
source code as you receive it, in any medium, provided that you
|
|
||||||
conspicuously and appropriately publish on each copy an appropriate
|
|
||||||
copyright notice and disclaimer of warranty; keep intact all the
|
|
||||||
notices that refer to this License and to the absence of any warranty;
|
|
||||||
and give any other recipients of the Program a copy of this License
|
|
||||||
along with the Program.
|
|
||||||
|
|
||||||
You may charge a fee for the physical act of transferring a copy, and
|
|
||||||
you may at your option offer warranty protection in exchange for a fee.
|
|
||||||
|
|
||||||
2. You may modify your copy or copies of the Program or any portion
|
|
||||||
of it, thus forming a work based on the Program, and copy and
|
|
||||||
distribute such modifications or work under the terms of Section 1
|
|
||||||
above, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) You must cause the modified files to carry prominent notices
|
|
||||||
stating that you changed the files and the date of any change.
|
|
||||||
|
|
||||||
b) You must cause any work that you distribute or publish, that in
|
|
||||||
whole or in part contains or is derived from the Program or any
|
|
||||||
part thereof, to be licensed as a whole at no charge to all third
|
|
||||||
parties under the terms of this License.
|
|
||||||
|
|
||||||
c) If the modified program normally reads commands interactively
|
|
||||||
when run, you must cause it, when started running for such
|
|
||||||
interactive use in the most ordinary way, to print or display an
|
|
||||||
announcement including an appropriate copyright notice and a
|
|
||||||
notice that there is no warranty (or else, saying that you provide
|
|
||||||
a warranty) and that users may redistribute the program under
|
|
||||||
these conditions, and telling the user how to view a copy of this
|
|
||||||
License. (Exception: if the Program itself is interactive but
|
|
||||||
does not normally print such an announcement, your work based on
|
|
||||||
the Program is not required to print an announcement.)
|
|
||||||
|
|
||||||
These requirements apply to the modified work as a whole. If
|
|
||||||
identifiable sections of that work are not derived from the Program,
|
|
||||||
and can be reasonably considered independent and separate works in
|
|
||||||
themselves, then this License, and its terms, do not apply to those
|
|
||||||
sections when you distribute them as separate works. But when you
|
|
||||||
distribute the same sections as part of a whole which is a work based
|
|
||||||
on the Program, the distribution of the whole must be on the terms of
|
|
||||||
this License, whose permissions for other licensees extend to the
|
|
||||||
entire whole, and thus to each and every part regardless of who wrote it.
|
|
||||||
|
|
||||||
Thus, it is not the intent of this section to claim rights or contest
|
|
||||||
your rights to work written entirely by you; rather, the intent is to
|
|
||||||
exercise the right to control the distribution of derivative or
|
|
||||||
collective works based on the Program.
|
|
||||||
|
|
||||||
In addition, mere aggregation of another work not based on the Program
|
|
||||||
with the Program (or with a work based on the Program) on a volume of
|
|
||||||
a storage or distribution medium does not bring the other work under
|
|
||||||
the scope of this License.
|
|
||||||
|
|
||||||
3. You may copy and distribute the Program (or a work based on it,
|
|
||||||
under Section 2) in object code or executable form under the terms of
|
|
||||||
Sections 1 and 2 above provided that you also do one of the following:
|
|
||||||
|
|
||||||
a) Accompany it with the complete corresponding machine-readable
|
|
||||||
source code, which must be distributed under the terms of Sections
|
|
||||||
1 and 2 above on a medium customarily used for software interchange; or,
|
|
||||||
|
|
||||||
b) Accompany it with a written offer, valid for at least three
|
|
||||||
years, to give any third party, for a charge no more than your
|
|
||||||
cost of physically performing source distribution, a complete
|
|
||||||
machine-readable copy of the corresponding source code, to be
|
|
||||||
distributed under the terms of Sections 1 and 2 above on a medium
|
|
||||||
customarily used for software interchange; or,
|
|
||||||
|
|
||||||
c) Accompany it with the information you received as to the offer
|
|
||||||
to distribute corresponding source code. (This alternative is
|
|
||||||
allowed only for noncommercial distribution and only if you
|
|
||||||
received the program in object code or executable form with such
|
|
||||||
an offer, in accord with Subsection b above.)
|
|
||||||
|
|
||||||
The source code for a work means the preferred form of the work for
|
|
||||||
making modifications to it. For an executable work, complete source
|
|
||||||
code means all the source code for all modules it contains, plus any
|
|
||||||
associated interface definition files, plus the scripts used to
|
|
||||||
control compilation and installation of the executable. However, as a
|
|
||||||
special exception, the source code distributed need not include
|
|
||||||
anything that is normally distributed (in either source or binary
|
|
||||||
form) with the major components (compiler, kernel, and so on) of the
|
|
||||||
operating system on which the executable runs, unless that component
|
|
||||||
itself accompanies the executable.
|
|
||||||
|
|
||||||
If distribution of executable or object code is made by offering
|
|
||||||
access to copy from a designated place, then offering equivalent
|
|
||||||
access to copy the source code from the same place counts as
|
|
||||||
distribution of the source code, even though third parties are not
|
|
||||||
compelled to copy the source along with the object code.
|
|
||||||
|
|
||||||
4. You may not copy, modify, sublicense, or distribute the Program
|
|
||||||
except as expressly provided under this License. Any attempt
|
|
||||||
otherwise to copy, modify, sublicense or distribute the Program is
|
|
||||||
void, and will automatically terminate your rights under this License.
|
|
||||||
However, parties who have received copies, or rights, from you under
|
|
||||||
this License will not have their licenses terminated so long as such
|
|
||||||
parties remain in full compliance.
|
|
||||||
|
|
||||||
5. You are not required to accept this License, since you have not
|
|
||||||
signed it. However, nothing else grants you permission to modify or
|
|
||||||
distribute the Program or its derivative works. These actions are
|
|
||||||
prohibited by law if you do not accept this License. Therefore, by
|
|
||||||
modifying or distributing the Program (or any work based on the
|
|
||||||
Program), you indicate your acceptance of this License to do so, and
|
|
||||||
all its terms and conditions for copying, distributing or modifying
|
|
||||||
the Program or works based on it.
|
|
||||||
|
|
||||||
6. Each time you redistribute the Program (or any work based on the
|
|
||||||
Program), the recipient automatically receives a license from the
|
|
||||||
original licensor to copy, distribute or modify the Program subject to
|
|
||||||
these terms and conditions. You may not impose any further
|
|
||||||
restrictions on the recipients' exercise of the rights granted herein.
|
|
||||||
You are not responsible for enforcing compliance by third parties to
|
|
||||||
this License.
|
|
||||||
|
|
||||||
7. If, as a consequence of a court judgment or allegation of patent
|
|
||||||
infringement or for any other reason (not limited to patent issues),
|
|
||||||
conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot
|
|
||||||
distribute so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you
|
|
||||||
may not distribute the Program at all. For example, if a patent
|
|
||||||
license would not permit royalty-free redistribution of the Program by
|
|
||||||
all those who receive copies directly or indirectly through you, then
|
|
||||||
the only way you could satisfy both it and this License would be to
|
|
||||||
refrain entirely from distribution of the Program.
|
|
||||||
|
|
||||||
If any portion of this section is held invalid or unenforceable under
|
|
||||||
any particular circumstance, the balance of the section is intended to
|
|
||||||
apply and the section as a whole is intended to apply in other
|
|
||||||
circumstances.
|
|
||||||
|
|
||||||
It is not the purpose of this section to induce you to infringe any
|
|
||||||
patents or other property right claims or to contest validity of any
|
|
||||||
such claims; this section has the sole purpose of protecting the
|
|
||||||
integrity of the free software distribution system, which is
|
|
||||||
implemented by public license practices. Many people have made
|
|
||||||
generous contributions to the wide range of software distributed
|
|
||||||
through that system in reliance on consistent application of that
|
|
||||||
system; it is up to the author/donor to decide if he or she is willing
|
|
||||||
to distribute software through any other system and a licensee cannot
|
|
||||||
impose that choice.
|
|
||||||
|
|
||||||
This section is intended to make thoroughly clear what is believed to
|
|
||||||
be a consequence of the rest of this License.
|
|
||||||
|
|
||||||
8. If the distribution and/or use of the Program is restricted in
|
|
||||||
certain countries either by patents or by copyrighted interfaces, the
|
|
||||||
original copyright holder who places the Program under this License
|
|
||||||
may add an explicit geographical distribution limitation excluding
|
|
||||||
those countries, so that distribution is permitted only in or among
|
|
||||||
countries not thus excluded. In such case, this License incorporates
|
|
||||||
the limitation as if written in the body of this License.
|
|
||||||
|
|
||||||
9. The Free Software Foundation may publish revised and/or new versions
|
|
||||||
of the General Public License from time to time. Such new versions will
|
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the Program
|
|
||||||
specifies a version number of this License which applies to it and "any
|
|
||||||
later version", you have the option of following the terms and conditions
|
|
||||||
either of that version or of any later version published by the Free
|
|
||||||
Software Foundation. If the Program does not specify a version number of
|
|
||||||
this License, you may choose any version ever published by the Free Software
|
|
||||||
Foundation.
|
|
||||||
|
|
||||||
10. If you wish to incorporate parts of the Program into other free
|
|
||||||
programs whose distribution conditions are different, write to the author
|
|
||||||
to ask for permission. For software which is copyrighted by the Free
|
|
||||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
|
||||||
make exceptions for this. Our decision will be guided by the two goals
|
|
||||||
of preserving the free status of all derivatives of our free software and
|
|
||||||
of promoting the sharing and reuse of software generally.
|
|
||||||
|
|
||||||
NO WARRANTY
|
|
||||||
|
|
||||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
|
||||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
|
||||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
|
||||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
|
||||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
||||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
|
||||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
|
||||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
|
||||||
REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
|
||||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
|
||||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
|
||||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
|
||||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
|
||||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
|
||||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
|
||||||
POSSIBILITY OF SUCH DAMAGES.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
convey the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation; either version 2 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along
|
|
||||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
|
||||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If the program is interactive, make it output a short notice like this
|
|
||||||
when it starts in an interactive mode:
|
|
||||||
|
|
||||||
Gnomovision version 69, Copyright (C) year name of author
|
|
||||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
||||||
This is free software, and you are welcome to redistribute it
|
|
||||||
under certain conditions; type `show c' for details.
|
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, the commands you use may
|
|
||||||
be called something other than `show w' and `show c'; they could even be
|
|
||||||
mouse-clicks or menu items--whatever suits your program.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or your
|
|
||||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
|
||||||
necessary. Here is a sample; alter the names:
|
|
||||||
|
|
||||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
|
||||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
|
||||||
|
|
||||||
<signature of Ty Coon>, 1 April 1989
|
|
||||||
Ty Coon, President of Vice
|
|
||||||
|
|
||||||
This General Public License does not permit incorporating your program into
|
|
||||||
proprietary programs. If your program is a subroutine library, you may
|
|
||||||
consider it more useful to permit linking proprietary applications with the
|
|
||||||
library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License.
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
|
||||||
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
|
|
||||||
kludge and added threading and context objects to it, wrote a proper OO
|
|
||||||
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
|
||||||
it with distutils, and generally transformed it from a quick hack to a
|
|
||||||
real, usable Python extension.
|
|
||||||
|
|
||||||
Other contributors (in random order):
|
|
||||||
|
|
||||||
Daniel Troeder
|
|
||||||
for pointing out a typo in @noreply
|
|
||||||
arkanes@irc.freenode.net
|
|
||||||
for suggesting a class method to compute and cache protocol masks
|
|
||||||
habnabit@habnabit.org
|
|
||||||
for suggesting function attributes and decorators for protocol negotiation
|
|
||||||
Dwayne Litzenberger, B.A.Sc.
|
|
||||||
for library_dirs patch to compile on Debian
|
|
||||||
Dave MacQuigg
|
|
||||||
for noticing that smfi_insheader wasn't supported, and creating
|
|
||||||
a template to help first time pymilter users create their own milter.
|
|
||||||
Terence Way
|
|
||||||
for providing a Python port of SPF
|
|
||||||
Scott Kitterman
|
|
||||||
for doing lots of testing and debugging of SPF against draft standard,
|
|
||||||
and for putting up a web page that validates SPF records using spf.py
|
|
||||||
Alexander Kourakos
|
|
||||||
for plugging several memory leaks
|
|
||||||
George Graf at Vienna University of Economics and Business Administration
|
|
||||||
for handling None passed to setreply and chgheader.
|
|
||||||
Deron Meranda
|
|
||||||
for IPv6 patches
|
|
||||||
Jason Erikson
|
|
||||||
for handling NULL hostaddr in connect callback.
|
|
||||||
John Draper
|
|
||||||
for porting Python milter to OpenBSD, and starting to work on tutorials
|
|
||||||
then pointing out that it would be easier to just write the MTA in Python.
|
|
||||||
Eric S. Johansson
|
|
||||||
for helpful design discussions while working on camram
|
|
||||||
Alex Savguira
|
|
||||||
for finding bugs with international headers and
|
|
||||||
suggesting the scan_zip option.
|
|
||||||
Business Management Systems - http://www.bmsi.com
|
|
||||||
for hosting the website, and providing paying clients who need milter service
|
|
||||||
so I can work on it as part of my day job.
|
|
||||||
|
|
||||||
If I have left anybody out, send me a reminder: stuart@gathman.org
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
# Revision 1.35 2013/03/14 22:11:25 customdesigned
|
|
||||||
# Release 0.9.8
|
|
||||||
#
|
|
||||||
# Revision 1.34 2013/03/09 05:42:14 customdesigned
|
|
||||||
# Make TestBase members private, fix getsymlist misspelling.
|
|
||||||
#
|
|
||||||
# Revision 1.33 2013/03/09 00:25:23 customdesigned
|
|
||||||
# Better untrapped exception message. const char for doc comments.
|
|
||||||
#
|
|
||||||
# Revision 1.32 2013/01/13 01:46:16 customdesigned
|
|
||||||
# Doc updates.
|
|
||||||
#
|
|
||||||
# Revision 1.31 2012/04/12 23:32:50 customdesigned
|
|
||||||
# Replace redundant callback array with macros. If this doesn't break anything,
|
|
||||||
# macros can be eliminated with code changes.
|
|
||||||
#
|
|
||||||
# Revision 1.30 2012/04/12 23:08:06 customdesigned
|
|
||||||
# Support RFC2553 on BSD
|
|
||||||
#
|
|
||||||
# Revision 1.29 2011/06/09 15:45:27 customdesigned
|
|
||||||
# Print callback name for non-int return error.
|
|
||||||
#
|
|
||||||
# Revision 1.28 2011/06/08 23:13:48 customdesigned
|
|
||||||
# Generate special exception when callback return not int.
|
|
||||||
#
|
|
||||||
# Revision 1.27 2009/07/28 21:45:54 customdesigned
|
|
||||||
# Add getversion() to return runtime version.
|
|
||||||
#
|
|
||||||
# Revision 1.26 2009/07/28 21:08:20 customdesigned
|
|
||||||
# Increment del count.
|
|
||||||
#
|
|
||||||
# Revision 1.25 2009/07/28 20:58:55 customdesigned
|
|
||||||
# getdiag method
|
|
||||||
#
|
|
||||||
# Revision 1.24 2009/06/09 01:54:44 customdesigned
|
|
||||||
# Forgot to initialize optional parameter.
|
|
||||||
#
|
|
||||||
# Revision 1.23 2009/05/29 20:44:58 customdesigned
|
|
||||||
# Typo SMFIP_NO constants.
|
|
||||||
#
|
|
||||||
# Revision 1.22 2009/05/29 19:53:36 customdesigned
|
|
||||||
# Typo SMFIS_ALL_OPTS
|
|
||||||
#
|
|
||||||
# Revision 1.21 2009/05/29 19:49:40 customdesigned
|
|
||||||
# Typo calling helo instead of negotiate.
|
|
||||||
#
|
|
||||||
# Revision 1.20 2009/05/29 18:25:59 customdesigned
|
|
||||||
# Null terminate keyword list.
|
|
||||||
#
|
|
||||||
# Revision 1.19 2009/05/28 18:36:42 customdesigned
|
|
||||||
# Support new callbacks, including negotiate
|
|
||||||
#
|
|
||||||
# Revision 1.18 2009/05/21 21:53:05 customdesigned
|
|
||||||
# First cut at support unknown, data, negotiate callbacks.
|
|
||||||
#
|
|
||||||
# Revision 1.17 2009/02/06 04:28:08 customdesigned
|
|
||||||
# Oops! Missing options argument pointer for addrcpt.
|
|
||||||
#
|
|
||||||
# Revision 1.16 2008/12/16 04:21:05 customdesigned
|
|
||||||
# Fedora release
|
|
||||||
#
|
|
||||||
# Revision 1.15 2008/12/13 20:29:56 customdesigned
|
|
||||||
# Split off milter applications.
|
|
||||||
#
|
|
||||||
# Revision 1.14 2008/12/04 19:43:00 customdesigned
|
|
||||||
# Doc updates.
|
|
||||||
#
|
|
||||||
# Revision 1.13 2008/11/23 03:06:47 customdesigned
|
|
||||||
# Milter support for chgfrom.
|
|
||||||
#
|
|
||||||
# Revision 1.12 2008/11/21 20:42:52 customdesigned
|
|
||||||
# Support smfi_chgfrom and smfi_addrcpt_par.
|
|
||||||
#
|
|
||||||
# Revision 1.11 2007/09/25 02:26:29 customdesigned
|
|
||||||
# Update license.
|
|
||||||
#
|
|
||||||
# Revision 1.10 2006/02/12 02:00:42 customdesigned
|
|
||||||
# Resolve FIXME for wrap_close.
|
|
||||||
#
|
|
||||||
# Revision 1.9 2005/12/23 21:46:36 customdesigned
|
|
||||||
# Compile on sendmail-8.12 (ifdef SMFIR_INSHEADER)
|
|
||||||
#
|
|
||||||
# Revision 1.8 2005/10/20 23:23:36 customdesigned
|
|
||||||
# Include smfi_progress is SMFIR_PROGRESS defined
|
|
||||||
#
|
|
||||||
# Revision 1.7 2005/10/20 23:04:46 customdesigned
|
|
||||||
# Add optional idx for position of added header.
|
|
||||||
#
|
|
||||||
# Revision 1.6 2005/07/15 22:18:17 customdesigned
|
|
||||||
# Support callback exception policy
|
|
||||||
#
|
|
||||||
# Revision 1.5 2005/06/24 04:20:07 customdesigned
|
|
||||||
# Report context allocation error.
|
|
||||||
#
|
|
||||||
# Revision 1.4 2005/06/24 04:12:43 customdesigned
|
|
||||||
# Remove unused name argument to generic wrappers.
|
|
||||||
#
|
|
||||||
# Revision 1.3 2005/06/24 03:57:35 customdesigned
|
|
||||||
# Handle close called before connect.
|
|
||||||
#
|
|
||||||
# Revision 1.2 2005/06/02 04:18:55 customdesigned
|
|
||||||
# Update copyright notices after reading article on /.
|
|
||||||
#
|
|
||||||
# Revision 1.1.1.2 2005/05/31 18:09:06 customdesigned
|
|
||||||
# Release 0.7.1
|
|
||||||
#
|
|
||||||
# Revision 2.31 2004/08/23 02:24:36 stuart
|
|
||||||
# Support setbacklog
|
|
||||||
#
|
|
||||||
# Revision 2.30 2004/08/21 20:29:53 stuart
|
|
||||||
# Support option of 11 lines max for mlreply.
|
|
||||||
#
|
|
||||||
# Revision 2.29 2004/08/21 04:14:29 stuart
|
|
||||||
# mlreply support
|
|
||||||
#
|
|
||||||
# Revision 2.28 2004/08/21 02:45:21 stuart
|
|
||||||
# Don't leak int constants if module unloaded.
|
|
||||||
#
|
|
||||||
# Revision 2.27 2004/04/06 03:19:59 stuart
|
|
||||||
# Release 0.6.8
|
|
||||||
#
|
|
||||||
# Revision 2.26 2004/03/04 21:43:06 stuart
|
|
||||||
# Fix memory leak by removing unused dynamic template buffer,
|
|
||||||
# thanks again to Alexander Kourakos.
|
|
||||||
#
|
|
||||||
# Revision 2.25 2004/03/01 19:45:03 stuart
|
|
||||||
# Release 0.6.5
|
|
||||||
#
|
|
||||||
# Revision 2.24 2004/03/01 18:56:50 stuart
|
|
||||||
# Support progress reporting.
|
|
||||||
#
|
|
||||||
# Revision 2.23 2004/03/01 18:36:09 stuart
|
|
||||||
# Plug memory leak. Thanks to Alexander Kourakos.
|
|
||||||
#
|
|
||||||
# Revision 2.22 2003/11/02 03:01:46 stuart
|
|
||||||
# Adjust SMTP error codes after careful reading of standard.
|
|
||||||
#
|
|
||||||
# Revision 2.21 2003/06/24 19:57:04 stuart
|
|
||||||
# Allow removing a python milter callback by setting to None.
|
|
||||||
#
|
|
||||||
# Revision 2.20 2003/02/13 17:08:57 stuart
|
|
||||||
# IPV6 support
|
|
||||||
#
|
|
||||||
# Revision 2.19 2003/02/13 16:58:29 stuart
|
|
||||||
# Support passing None to setreply and chgheader.
|
|
||||||
#
|
|
||||||
# Revision 2.18 2002/12/11 16:44:06 stuart
|
|
||||||
# Support QUARANTINE if supported by libmilter.
|
|
||||||
#
|
|
||||||
# Revision 2.17 2002/04/18 20:20:35 stuart
|
|
||||||
# Fix for NULL hostaddr in connect callback from Jason Erickson.
|
|
||||||
#
|
|
||||||
# Revision 2.16 2001/09/26 13:29:09 stuart
|
|
||||||
# sa_len not supported by linux.
|
|
||||||
#
|
|
||||||
# Revision 2.15 2001/09/25 17:28:40 stuart
|
|
||||||
# Copyrights, documentation, release 0.3.1
|
|
||||||
#
|
|
||||||
# Revision 2.14 2001/09/25 00:36:57 stuart
|
|
||||||
# Pass hostaddr to python code in format used by standard socket module.
|
|
||||||
#
|
|
||||||
# Revision 2.13 2001/09/24 23:44:55 stuart
|
|
||||||
# Return old callback from setcallback functions.
|
|
||||||
#
|
|
||||||
# Revision 2.12 2001/09/24 20:02:30 stuart
|
|
||||||
# Remove redundant setpriv
|
|
||||||
#
|
|
||||||
# Revision 2.11 2001/09/23 22:26:35 stuart
|
|
||||||
# Update docs. Streamline Milter.py
|
|
||||||
# update testbms.py to reflect actual sendmail behaviour with multiple
|
|
||||||
# messages per connection.
|
|
||||||
#
|
|
||||||
# Revision 2.10 2001/09/22 15:33:42 stuart
|
|
||||||
# More doc comment updates.
|
|
||||||
#
|
|
||||||
# Revision 2.9 2001/09/22 14:52:27 stuart
|
|
||||||
# Actually return retval in _generic_return.
|
|
||||||
# Go over doc comments.
|
|
||||||
#
|
|
||||||
# Revision 2.8 2001/09/22 01:59:32 stuart
|
|
||||||
# Prevent reentrant call of milter_main, which libmilter doesn't support.
|
|
||||||
#
|
|
||||||
# Revision 2.7 2001/09/22 01:47:37 stuart
|
|
||||||
# Forgot to set milter interp.
|
|
||||||
#
|
|
||||||
# Revision 2.6 2001/09/22 01:23:53 stuart
|
|
||||||
# Added proper threading after research in python docs.
|
|
||||||
#
|
|
||||||
# Revision 2.5 2001/09/21 20:08:51 stuart
|
|
||||||
# Release 0.2.3
|
|
||||||
#
|
|
||||||
# Revision 2.4 2001/09/20 16:18:16 stuart
|
|
||||||
# libmilter checks in_eom state, so we don't have to.
|
|
||||||
#
|
|
||||||
# Revision 2.3 2001/09/19 06:02:33 stuart
|
|
||||||
# Make more stuff static.
|
|
||||||
#
|
|
||||||
# Revision 2.1 2001/09/19 04:24:13 stuart
|
|
||||||
# Use extension type to track context in python.
|
|
||||||
#
|
|
||||||
# Revision 1.4 2001/09/18 18:48:28 stuart
|
|
||||||
# clear private data reference in _clear_context
|
|
||||||
#
|
|
||||||
# Revision 1.3 2001/09/15 04:19:37 stuart
|
|
||||||
# nasty off by 1 mem overwrite bugs in wrap_env
|
|
||||||
# generic_set_callback
|
|
||||||
#
|
|
||||||
# Revision 1.2 2001/09/15 03:15:39 stuart
|
|
||||||
# several bugs fixed, works smoothly
|
|
||||||
#
|
|
||||||
# Revision 1.69 2006/11/04 22:09:39 customdesigned
|
|
||||||
# Another lame DSN heuristic. Block PTR cache poisoning attack.
|
|
||||||
#
|
|
||||||
# Revision 1.68 2006/10/04 03:46:01 customdesigned
|
|
||||||
# Fix defaults.
|
|
||||||
#
|
|
||||||
# Revision 1.67 2006/10/01 01:44:06 customdesigned
|
|
||||||
# case_sensitive_localpart option, more delayed bounce heuristics,
|
|
||||||
# optional smart_alias section.
|
|
||||||
#
|
|
||||||
# Revision 1.66 2006/07/26 16:42:26 customdesigned
|
|
||||||
# Support CBV timeout
|
|
||||||
#
|
|
||||||
# Revision 1.65 2006/06/21 22:22:00 customdesigned
|
|
||||||
# Handle multi-line headers in delayed dsns.
|
|
||||||
#
|
|
||||||
# Revision 1.64 2006/06/21 21:12:04 customdesigned
|
|
||||||
# More delayed reject token headers.
|
|
||||||
# Don't require HELO pass for CBV.
|
|
||||||
#
|
|
||||||
# Revision 1.63 2006/05/21 03:41:44 customdesigned
|
|
||||||
# Fail dsn
|
|
||||||
#
|
|
||||||
# Revision 1.61 2006/05/17 21:28:07 customdesigned
|
|
||||||
# Create GOSSiP record only when connection will procede to DATA.
|
|
||||||
#
|
|
||||||
# Revision 1.60 2006/05/12 16:14:48 customdesigned
|
|
||||||
# Don't require SPF pass for white/black listing mail from trusted relay.
|
|
||||||
# Support localpart wildcard for white and black lists.
|
|
||||||
#
|
|
||||||
# Revision 1.59 2006/04/06 18:14:17 customdesigned
|
|
||||||
# Check whitelist/blacklist even when not checking SPF (e.g. trusted relay).
|
|
||||||
#
|
|
||||||
# Revision 1.58 2006/03/10 20:52:49 customdesigned
|
|
||||||
# Use re to recognize failure DSNs.
|
|
||||||
#
|
|
||||||
# Revision 1.57 2006/03/07 20:50:54 customdesigned
|
|
||||||
# Use signed Message-ID in delayed reject to blacklist senders
|
|
||||||
#
|
|
||||||
# Revision 1.56 2006/02/24 02:12:54 customdesigned
|
|
||||||
# Properly report hard PermError (lax mode fails also) by always setting
|
|
||||||
# perm_error attribute with PermError exception. Improve reporting of
|
|
||||||
# invalid domain PermError.
|
|
||||||
#
|
|
||||||
# Revision 1.55 2006/02/17 05:04:29 customdesigned
|
|
||||||
# Use SRS sign domain list.
|
|
||||||
# Accept but do not use for training whitelisted senders without SPF pass.
|
|
||||||
# Immediate rejection of unsigned bounces.
|
|
||||||
#
|
|
||||||
# Revision 1.54 2006/02/16 02:16:36 customdesigned
|
|
||||||
# User specific SPF receiver policy.
|
|
||||||
#
|
|
||||||
# Revision 1.53 2006/02/12 04:15:01 customdesigned
|
|
||||||
# Remove spf dependency for iniplist
|
|
||||||
#
|
|
||||||
# Revision 1.52 2006/02/12 02:12:08 customdesigned
|
|
||||||
# Use CIDR notation for internal connect list.
|
|
||||||
#
|
|
||||||
# Revision 1.51 2006/02/12 01:13:58 customdesigned
|
|
||||||
# Don't check rcpt user list when signed MFROM.
|
|
||||||
#
|
|
||||||
# Revision 1.50 2006/02/09 20:39:43 customdesigned
|
|
||||||
# Use CIDR notation for trusted_relay iplist
|
|
||||||
#
|
|
||||||
# Revision 1.49 2006/01/30 23:14:48 customdesigned
|
|
||||||
# put back eom condition
|
|
||||||
#
|
|
||||||
# Revision 1.48 2006/01/12 20:31:24 customdesigned
|
|
||||||
# Accelerate training via whitelist and blacklist.
|
|
||||||
#
|
|
||||||
# Revision 1.47 2005/12/29 04:49:10 customdesigned
|
|
||||||
# Do not auto-whitelist autoreplys
|
|
||||||
#
|
|
||||||
# Revision 1.46 2005/12/28 20:17:29 customdesigned
|
|
||||||
# Expire and renew AddrCache entries
|
|
||||||
#
|
|
||||||
# Revision 1.45 2005/12/23 22:34:46 customdesigned
|
|
||||||
# Put guessed result in separate header.
|
|
||||||
#
|
|
||||||
# Revision 1.44 2005/12/23 21:47:07 customdesigned
|
|
||||||
# Move Received-SPF header to top.
|
|
||||||
#
|
|
||||||
# Revision 1.43 2005/12/09 16:54:01 customdesigned
|
|
||||||
# Select neutral DSN template for best_guess
|
|
||||||
#
|
|
||||||
# Revision 1.42 2005/12/01 22:42:32 customdesigned
|
|
||||||
# improve gossip support.
|
|
||||||
# Initialize srs_domain from srs.srs config property. Should probably
|
|
||||||
# always block unsigned DSN when signing all.
|
|
||||||
#
|
|
||||||
# Revision 1.41 2005/12/01 18:59:25 customdesigned
|
|
||||||
# Fix neutral policy. pobox.com -> openspf.org
|
|
||||||
#
|
|
||||||
# Revision 1.40 2005/11/07 21:22:35 customdesigned
|
|
||||||
# GOSSiP support, local database only.
|
|
||||||
#
|
|
||||||
# Revision 1.39 2005/10/31 00:04:58 customdesigned
|
|
||||||
# Simple implementation of trusted_forwarder list. Inefficient for
|
|
||||||
# more than 1 or 2 entries.
|
|
||||||
#
|
|
||||||
# Revision 1.38 2005/10/28 19:36:54 customdesigned
|
|
||||||
# Don't check internal_domains for trusted_relay.
|
|
||||||
#
|
|
||||||
# Revision 1.37 2005/10/28 09:30:49 customdesigned
|
|
||||||
# Do not send quarantine DSN when sender is DSN.
|
|
||||||
#
|
|
||||||
# Revision 1.36 2005/10/23 16:01:29 customdesigned
|
|
||||||
# Consider MAIL FROM a match for supply_sender when a subdomain of From or Sender
|
|
||||||
#
|
|
||||||
# Revision 1.35 2005/10/20 18:47:27 customdesigned
|
|
||||||
# Configure auto_whitelist senders.
|
|
||||||
#
|
|
||||||
# Revision 1.34 2005/10/19 21:07:49 customdesigned
|
|
||||||
# access.db stores keys in lower case
|
|
||||||
#
|
|
||||||
# Revision 1.33 2005/10/19 19:37:50 customdesigned
|
|
||||||
# Train screener on whitelisted messages.
|
|
||||||
#
|
|
||||||
# Revision 1.32 2005/10/14 16:17:31 customdesigned
|
|
||||||
# Auto whitelist refinements.
|
|
||||||
#
|
|
||||||
# Revision 1.31 2005/10/14 01:14:08 customdesigned
|
|
||||||
# Auto whitelist feature.
|
|
||||||
#
|
|
||||||
# Revision 1.30 2005/10/12 16:36:30 customdesigned
|
|
||||||
# Release 0.8.3
|
|
||||||
#
|
|
||||||
# Revision 1.29 2005/10/11 22:50:07 customdesigned
|
|
||||||
# Always check HELO except for SPF pass, temperror.
|
|
||||||
#
|
|
||||||
# Revision 1.28 2005/10/10 23:50:20 customdesigned
|
|
||||||
# Use logging module to make logging threadsafe (avoid splitting log lines)
|
|
||||||
#
|
|
||||||
# Revision 1.27 2005/10/10 20:15:33 customdesigned
|
|
||||||
# Configure SPF policy via sendmail access file.
|
|
||||||
#
|
|
||||||
# Revision 1.26 2005/10/07 03:23:40 customdesigned
|
|
||||||
# Banned users option. Experimental feature to supply Sender when
|
|
||||||
# missing and MFROM domain doesn't match From. Log cipher bits for
|
|
||||||
# SMTP AUTH. Sketch access file feature.
|
|
||||||
#
|
|
||||||
# Revision 1.25 2005/09/08 03:55:08 customdesigned
|
|
||||||
# Handle perverse MFROM quoting.
|
|
||||||
#
|
|
||||||
# Revision 1.24 2005/08/18 03:36:54 customdesigned
|
|
||||||
# Don't innoculate with SCREENED mail.
|
|
||||||
#
|
|
||||||
# Revision 1.23 2005/08/17 19:35:27 customdesigned
|
|
||||||
# Send DSN before adding message to quarantine.
|
|
||||||
#
|
|
||||||
# Revision 1.22 2005/08/11 22:17:58 customdesigned
|
|
||||||
# Consider SMTP AUTH connections internal.
|
|
||||||
#
|
|
||||||
# Revision 1.21 2005/08/04 21:21:31 customdesigned
|
|
||||||
# Treat fail like softfail for selected (braindead) domains.
|
|
||||||
# Treat mail according to extended processing results, but
|
|
||||||
# report any PermError that would officially result via DSN.
|
|
||||||
#
|
|
||||||
# Revision 1.20 2005/08/02 18:04:35 customdesigned
|
|
||||||
# Keep screened honeypot mail, but optionally discard honeypot only mail.
|
|
||||||
#
|
|
||||||
# Revision 1.19 2005/07/20 03:30:04 customdesigned
|
|
||||||
# Check pydspam version for honeypot, include latest pyspf changes.
|
|
||||||
#
|
|
||||||
# Revision 1.18 2005/07/17 01:25:44 customdesigned
|
|
||||||
# Log as well as use extended result for best guess.
|
|
||||||
#
|
|
||||||
# Revision 1.17 2005/07/15 20:25:36 customdesigned
|
|
||||||
# Use extended results processing for best_guess.
|
|
||||||
#
|
|
||||||
# Revision 1.16 2005/07/14 03:23:33 customdesigned
|
|
||||||
# Make SES package optional. Initial honeypot support.
|
|
||||||
#
|
|
||||||
# Revision 1.15 2005/07/06 04:05:40 customdesigned
|
|
||||||
# Initial SES integration.
|
|
||||||
#
|
|
||||||
# Revision 1.14 2005/07/02 23:27:31 customdesigned
|
|
||||||
# Don't match hostnames for internal connects.
|
|
||||||
#
|
|
||||||
# Revision 1.13 2005/07/01 16:30:24 customdesigned
|
|
||||||
# Always log trusted Received and Received-SPF headers.
|
|
||||||
#
|
|
||||||
# Revision 1.12 2005/06/20 22:35:35 customdesigned
|
|
||||||
# Setreply for rejectvirus.
|
|
||||||
#
|
|
||||||
# Revision 1.11 2005/06/17 02:07:20 customdesigned
|
|
||||||
# Release 0.8.1
|
|
||||||
#
|
|
||||||
# Revision 1.10 2005/06/16 18:35:51 customdesigned
|
|
||||||
# Ignore HeaderParseError decoding header
|
|
||||||
#
|
|
||||||
# Revision 1.9 2005/06/14 21:55:29 customdesigned
|
|
||||||
# Check internal_domains for outgoing mail.
|
|
||||||
#
|
|
||||||
# Revision 1.8 2005/06/06 18:24:59 customdesigned
|
|
||||||
# Properly log exceptions from pydspam
|
|
||||||
#
|
|
||||||
# Revision 1.7 2005/06/04 19:41:16 customdesigned
|
|
||||||
# Fix bugs from testing RPM
|
|
||||||
#
|
|
||||||
# Revision 1.6 2005/06/03 04:57:05 customdesigned
|
|
||||||
# Organize config reader by section. Create defang section.
|
|
||||||
#
|
|
||||||
# Revision 1.5 2005/06/02 15:00:17 customdesigned
|
|
||||||
# Configure banned extensions. Scan zipfile option with test case.
|
|
||||||
#
|
|
||||||
# Revision 1.4 2005/06/02 04:18:55 customdesigned
|
|
||||||
# Update copyright notices after reading article on /.
|
|
||||||
#
|
|
||||||
# Revision 1.3 2005/06/02 02:09:00 customdesigned
|
|
||||||
# Record timestamp in send_dsn.log
|
|
||||||
#
|
|
||||||
# Revision 1.2 2005/06/02 01:00:36 customdesigned
|
|
||||||
# Support configurable templates for DSNs.
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
include COPYING
|
|
||||||
include TODO
|
|
||||||
include NEWS
|
|
||||||
include CREDITS
|
|
||||||
include README.md
|
|
||||||
include ChangeLog
|
|
||||||
include MANIFEST.in
|
|
||||||
include testsample.py
|
|
||||||
include testmime.py
|
|
||||||
include testutils.py
|
|
||||||
include test.py
|
|
||||||
include sample.py
|
|
||||||
include milter-template.py
|
|
||||||
include test/*
|
|
||||||
include Milter/*.py
|
|
||||||
include *.spec
|
|
||||||
include start.sh
|
|
||||||
@@ -1,894 +0,0 @@
|
|||||||
## @package Milter
|
|
||||||
# A thin OO wrapper for the milter module.
|
|
||||||
#
|
|
||||||
# Clients generally subclass Milter.Base and define callback
|
|
||||||
# methods.
|
|
||||||
#
|
|
||||||
# @author Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2001,2009 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
__version__ = '1.0.5'
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import milter
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
import thread
|
|
||||||
except:
|
|
||||||
# libmilter uses posix threads
|
|
||||||
import _thread as thread
|
|
||||||
|
|
||||||
from milter import *
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
_seq_lock = thread.allocate_lock()
|
|
||||||
_seq = 0
|
|
||||||
|
|
||||||
def uniqueID():
|
|
||||||
"""Return a unique sequence number (incremented on each call).
|
|
||||||
"""
|
|
||||||
global _seq
|
|
||||||
_seq_lock.acquire()
|
|
||||||
seqno = _seq = _seq + 1
|
|
||||||
_seq_lock.release()
|
|
||||||
return seqno
|
|
||||||
|
|
||||||
## @private
|
|
||||||
OPTIONAL_CALLBACKS = {
|
|
||||||
'connect':(P_NR_CONN,P_NOCONNECT),
|
|
||||||
'hello':(P_NR_HELO,P_NOHELO),
|
|
||||||
'envfrom':(P_NR_MAIL,P_NOMAIL),
|
|
||||||
'envrcpt':(P_NR_RCPT,P_NORCPT),
|
|
||||||
'data':(P_NR_DATA,P_NODATA),
|
|
||||||
'unknown':(P_NR_UNKN,P_NOUNKNOWN),
|
|
||||||
'eoh':(P_NR_EOH,P_NOEOH),
|
|
||||||
'body':(P_NR_BODY,P_NOBODY),
|
|
||||||
'header':(P_NR_HDR,P_NOHDRS)
|
|
||||||
}
|
|
||||||
|
|
||||||
MACRO_CALLBACKS = {
|
|
||||||
'connect': M_CONNECT,
|
|
||||||
'hello': M_HELO, 'envfrom': M_ENVFROM, 'envrcpt': M_ENVRCPT,
|
|
||||||
'data': M_DATA, 'eom': M_EOM, 'eoh': M_EOH
|
|
||||||
}
|
|
||||||
|
|
||||||
## @private
|
|
||||||
R = re.compile(r'%+')
|
|
||||||
|
|
||||||
## @private
|
|
||||||
def decode_mask(bits,names):
|
|
||||||
t = [ (s,getattr(milter,s)) for s in names]
|
|
||||||
nms = [s for s,m in t if bits & m]
|
|
||||||
for s,m in t: bits &= ~m
|
|
||||||
if bits: nms += hex(bits)
|
|
||||||
return nms
|
|
||||||
|
|
||||||
## Class decorator to enable optional protocol steps.
|
|
||||||
# P_SKIP is enabled by default when supported, but
|
|
||||||
# applications may wish to enable P_HDR_LEADSPC
|
|
||||||
# to send and receive the leading space of header continuation
|
|
||||||
# lines unchanged, and/or P_RCPT_REJ to have recipients
|
|
||||||
# detected as invalid by the MTA passed to the envcrpt callback.
|
|
||||||
#
|
|
||||||
# Applications may want to check whether the protocol is actually
|
|
||||||
# supported by the MTA in use. Base._protocol
|
|
||||||
# is a bitmask of protocol options negotiated. So,
|
|
||||||
# for instance, if <code>self._protocol & Milter.P_RCPT_REJ</code>
|
|
||||||
# is true, then that feature was successfully negotiated with the MTA
|
|
||||||
# and the application will see recipients the MTA has flagged as invalid.
|
|
||||||
#
|
|
||||||
# Sample use:
|
|
||||||
# <pre>
|
|
||||||
# class myMilter(Milter.Base):
|
|
||||||
# def envrcpt(self,to,*params):
|
|
||||||
# return Milter.CONTINUE
|
|
||||||
# myMilter = Milter.enable_protocols(myMilter,Milter.P_RCPT_REJ)
|
|
||||||
# </pre>
|
|
||||||
# @since 0.9.3
|
|
||||||
# @param klass the %milter application class to modify
|
|
||||||
# @param mask a bitmask of protocol steps to enable
|
|
||||||
# @return the modified %milter class
|
|
||||||
def enable_protocols(klass,mask):
|
|
||||||
klass._protocol_mask = klass.protocol_mask() & ~mask
|
|
||||||
return klass
|
|
||||||
|
|
||||||
## Milter rejected recipients. A class decorator that calls
|
|
||||||
# enable_protocols() with the P_RCPT_REJ flag. By default, the MTA
|
|
||||||
# does not pass recipients that it knows are invalid on to the milter.
|
|
||||||
# This decorator enables a %milter app to see all recipients if supported
|
|
||||||
# by the MTA. Use like this with python-2.6 and later:
|
|
||||||
# <pre>
|
|
||||||
# @@Milter.rejected_recipients
|
|
||||||
# class myMilter(Milter.Base):
|
|
||||||
# def envrcpt(self,to,*params):
|
|
||||||
# return Milter.CONTINUE
|
|
||||||
# </pre>
|
|
||||||
# @since 0.9.5
|
|
||||||
# @param klass the %milter application class to modify
|
|
||||||
# @return the modified %milter class
|
|
||||||
def rejected_recipients(klass):
|
|
||||||
return enable_protocols(klass,P_RCPT_REJ)
|
|
||||||
|
|
||||||
## Milter leading space on headers. A class decorator that calls
|
|
||||||
# enable_protocols() with the P_HDR_LEADSPC flag. By default,
|
|
||||||
# header continuation lines are collected and joined before getting
|
|
||||||
# sent to a milter. Headers modified or added by the milter are
|
|
||||||
# folded by the MTA as necessary according to its own standards.
|
|
||||||
# With this flag, header continuation lines are preserved
|
|
||||||
# with their newlines and leading space. In addition, header folding
|
|
||||||
# done by the milter is preserved as well.
|
|
||||||
# Use like this with python-2.6 and later:
|
|
||||||
# <pre>
|
|
||||||
# @@Milter.header_leading_space
|
|
||||||
# class myMilter(Milter.Base):
|
|
||||||
# def header(self,hname,value):
|
|
||||||
# return Milter.CONTINUE
|
|
||||||
# </pre>
|
|
||||||
# @since 0.9.5
|
|
||||||
# @param klass the %milter application class to modify
|
|
||||||
# @return the modified %milter class
|
|
||||||
def header_leading_space(klass):
|
|
||||||
return enable_protocols(klass,P_HDR_LEADSPC)
|
|
||||||
|
|
||||||
## Function decorator to disable callback methods.
|
|
||||||
# If the MTA supports it, tells the MTA not to invoke this callback,
|
|
||||||
# increasing efficiency. All the callbacks (except negotiate)
|
|
||||||
# are disabled in Milter.Base, and overriding them reenables the
|
|
||||||
# callback. An application may need to use @@nocallback when it extends
|
|
||||||
# another %milter and wants to disable a callback again.
|
|
||||||
# The disabled method should still return Milter.CONTINUE, in case the MTA does
|
|
||||||
# not support protocol negotiation, and for when called from a test harness.
|
|
||||||
# @since 0.9.2
|
|
||||||
def nocallback(func):
|
|
||||||
try:
|
|
||||||
func.milter_protocol = OPTIONAL_CALLBACKS[func.__name__][1]
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError(
|
|
||||||
'@nocallback applied to non-optional method: '+func.__name__)
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(self,*args):
|
|
||||||
if func(self,*args) != CONTINUE:
|
|
||||||
raise RuntimeError('%s return code must be CONTINUE with @nocallback'
|
|
||||||
% func.__name__)
|
|
||||||
return CONTINUE
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
## Function decorator to disable callback reply.
|
|
||||||
# If the MTA supports it, tells the MTA not to wait for a reply from
|
|
||||||
# this callback, and assume CONTINUE. The method should still return
|
|
||||||
# CONTINUE in case the MTA does not support protocol negotiation.
|
|
||||||
# The decorator arranges to change the return code to NOREPLY
|
|
||||||
# when supported by the MTA.
|
|
||||||
# @since 0.9.2
|
|
||||||
def noreply(func):
|
|
||||||
try:
|
|
||||||
nr_mask = OPTIONAL_CALLBACKS[func.__name__][0]
|
|
||||||
except KeyError:
|
|
||||||
raise ValueError(
|
|
||||||
'@noreply applied to non-optional method: '+func.__name__)
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(self,*args):
|
|
||||||
rc = func(self,*args)
|
|
||||||
if self._protocol & nr_mask:
|
|
||||||
if rc != CONTINUE:
|
|
||||||
raise RuntimeError('%s return code must be CONTINUE with @noreply'
|
|
||||||
% func.__name__)
|
|
||||||
return NOREPLY
|
|
||||||
return rc
|
|
||||||
wrapper.milter_protocol = nr_mask
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
## Function decorator to set decoding error strategy.
|
|
||||||
# Current RFCs define UTF-8 as the standard encoding for SMTP
|
|
||||||
# envelope and header fields. By default, Milter.Base decodes
|
|
||||||
# envelope and header values with errors='surrogateescape'.
|
|
||||||
# Applications can recover the original bytes with
|
|
||||||
# <pre>
|
|
||||||
# b = s.encode(errors='surrogateescape')
|
|
||||||
# </pre>
|
|
||||||
# This preserves information, but can lead to unexpected exceptions
|
|
||||||
# as you cannot, e.g. print strings with surrogates.
|
|
||||||
# Illegal bytes occur quite often in real life, so there must
|
|
||||||
# be a way to deal with them.
|
|
||||||
# This decorator can change the error strategy to
|
|
||||||
# <ul>
|
|
||||||
# <li> bytes - original bytes are passed unmodified
|
|
||||||
# <li> strict - pass bytes if illegal bytes are present, string otherwise
|
|
||||||
# <li> ignore - illegal bytes are removed
|
|
||||||
# <li> replace - illegal bytes are replaced with a unicode error symbol
|
|
||||||
# </ul>
|
|
||||||
#
|
|
||||||
def decode(strategy):
|
|
||||||
def setstrategy(func):
|
|
||||||
func.error_strategy = strategy
|
|
||||||
return func
|
|
||||||
return setstrategy
|
|
||||||
|
|
||||||
## Function decorator to set macros used in a callback.
|
|
||||||
# By default, the MTA sends all macros defined for a callback.
|
|
||||||
# If some or all of these are unused, the bandwidth can be saved
|
|
||||||
# by listing the ones that are used.
|
|
||||||
# @since 1.0.2
|
|
||||||
def symlist(*syms):
|
|
||||||
def setsyms(func):
|
|
||||||
if len(syms) > 5:
|
|
||||||
raise ValueError('@symlist limited to 5 macros by MTA: '+func.__name__)
|
|
||||||
if func.__name__ not in MACRO_CALLBACKS:
|
|
||||||
raise ValueError('@symlist applied to non-symlist method: '+func.__name__)
|
|
||||||
func._symlist = syms
|
|
||||||
return func
|
|
||||||
return setsyms
|
|
||||||
|
|
||||||
## Disabled action exception.
|
|
||||||
# set_flags() can tell the MTA that this application will not use certain
|
|
||||||
# features (such as CHGFROM). This can also be negotiated for each
|
|
||||||
# connection in the negotiate callback. If the application then calls
|
|
||||||
# the feature anyway via an instance method, this exception is
|
|
||||||
# thrown.
|
|
||||||
# @since 0.9.2
|
|
||||||
class DisabledAction(RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
## A do "nothing" Milter base class representing an SMTP connection.
|
|
||||||
#
|
|
||||||
# Python milters should derive from this class
|
|
||||||
# unless they are using the low level milter module directly.
|
|
||||||
#
|
|
||||||
# Most of the methods are either "actions" or "callbacks". Callbacks
|
|
||||||
# are invoked by the MTA at certain points in the SMTP protocol. For
|
|
||||||
# instance when the HELO command is seen, the MTA calls the helo
|
|
||||||
# callback before returning a response code. All callbacks must
|
|
||||||
# return one of these constants: CONTINUE, TEMPFAIL, REJECT, ACCEPT,
|
|
||||||
# DISCARD, SKIP. The NOREPLY response is supplied automatically by
|
|
||||||
# the @@noreply decorator if negotiation with the MTA is successful.
|
|
||||||
# @@noreply and @@nocallback methods should return CONTINUE for two reasons:
|
|
||||||
# the MTA may not support negotiation, and the class may be running in a test
|
|
||||||
# harness.
|
|
||||||
#
|
|
||||||
# Optional callbacks are disabled with the @@nocallback decorator, and
|
|
||||||
# automatically reenabled when overridden. Disabled callbacks should
|
|
||||||
# still return CONTINUE for testing and MTAs that do not support
|
|
||||||
# negotiation.
|
|
||||||
|
|
||||||
# Each SMTP connection to the MTA calls the factory method you provide to
|
|
||||||
# create an instance derived from this class. This is typically the
|
|
||||||
# constructor for a class derived from Base. The _setctx() method attaches
|
|
||||||
# the instance to the low level milter.milterContext object. When the SMTP
|
|
||||||
# connection terminates, the close callback is called, the low level connection
|
|
||||||
# object is destroyed, and this normally causes instances of this class to be
|
|
||||||
# garbage collected as well. The close() method should release any global
|
|
||||||
# resources held by instances.
|
|
||||||
# @since 0.9.2
|
|
||||||
class Base(object):
|
|
||||||
"The core class interface to the %milter module."
|
|
||||||
|
|
||||||
## Attach this Milter to the low level milter.milterContext object.
|
|
||||||
def _setctx(self,ctx):
|
|
||||||
## The low level @ref milter.milterContext object.
|
|
||||||
self._ctx = ctx
|
|
||||||
## A bitmask of actions this connection has negotiated to use.
|
|
||||||
# By default, all actions are enabled. High throughput milters
|
|
||||||
# may want to disable unused actions to increase efficiency.
|
|
||||||
# Some optional actions may be disabled by calling milter.set_flags(), or
|
|
||||||
# by overriding the negotiate callback. The bits include:
|
|
||||||
# <code>ADDHDRS,CHGBODY,MODBODY,ADDRCPT,ADDRCPT_PAR,DELRCPT
|
|
||||||
# CHGHDRS,QUARANTINE,CHGFROM,SETSYMLIST</code>.
|
|
||||||
# The <code>Milter.CURR_ACTS</code> bitmask is all actions
|
|
||||||
# known when the milter module was compiled.
|
|
||||||
# Application code can also inspect this field to determine
|
|
||||||
# which actions are available. This is especially useful in
|
|
||||||
# generic library code designed to work in multiple milters.
|
|
||||||
# @since 0.9.2
|
|
||||||
#
|
|
||||||
self._actions = CURR_ACTS # all actions enabled by default
|
|
||||||
## A bitmask of protocol options this connection has negotiated.
|
|
||||||
# An application may inspect this
|
|
||||||
# variable to determine which protocol steps are supported. Options
|
|
||||||
# of interest to applications: the SKIP result code is allowed
|
|
||||||
# only if the P_SKIP bit is set, rejected recipients are passed to the
|
|
||||||
# %milter application only if the P_RCPT_REJ bit is set, and
|
|
||||||
# header values are sent and received with leading spaces (in the
|
|
||||||
# continuation lines) intact if the P_HDR_LEADSPC bit is set (so
|
|
||||||
# that the application can customize indenting).
|
|
||||||
#
|
|
||||||
# The P_N* bits should be negotiated via the @@noreply and @@nocallback
|
|
||||||
# method decorators, and P_RCPT_REJ, P_HDR_LEADSPC should
|
|
||||||
# be enabled using the enable_protocols class decorator.
|
|
||||||
#
|
|
||||||
# The bits include: <code>
|
|
||||||
# P_RCPT_REJ P_NR_CONN P_NR_HELO P_NR_MAIL P_NR_RCPT P_NR_DATA P_NR_UNKN
|
|
||||||
# P_NR_EOH P_NR_BODY P_NR_HDR P_NOCONNECT P_NOHELO P_NOMAIL P_NORCPT
|
|
||||||
# P_NODATA P_NOUNKNOWN P_NOEOH P_NOBODY P_NOHDRS P_HDR_LEADSPC P_SKIP
|
|
||||||
# </code> (all under the Milter namespace).
|
|
||||||
# @since 0.9.2
|
|
||||||
self._protocol = 0 # no protocol options by default
|
|
||||||
if ctx:
|
|
||||||
ctx.setpriv(self)
|
|
||||||
|
|
||||||
## Defined by subclasses to write log messages.
|
|
||||||
def log(self,*msg): pass
|
|
||||||
## Called for each connection to the MTA. Called by the
|
|
||||||
# <a href="milter_api/xxfi_connect.html">
|
|
||||||
# xxfi_connect</a> callback.
|
|
||||||
# The <code>hostname</code> provided by the local MTA is either
|
|
||||||
# the PTR name or the IP in the form "[1.2.3.4]" if no PTR is available.
|
|
||||||
# The format of hostaddr depends on the socket family:
|
|
||||||
# <dl>
|
|
||||||
# <dt><code>socket.AF_INET</code>
|
|
||||||
# <dd>A tuple of (IP as string in dotted quad form, integer port)
|
|
||||||
# <dt><code>socket.AF_INET6</code>
|
|
||||||
# <dd>A tuple of (IP as a string in standard representation,
|
|
||||||
# integer port, integer flow info, integer scope id)
|
|
||||||
# <dt><code>socket.AF_UNIX</code>
|
|
||||||
# <dd>A string with the socketname
|
|
||||||
# </dl>
|
|
||||||
# To vary behavior based on what port the client connected to,
|
|
||||||
# for example skipping blacklist checks for port 587 (which must
|
|
||||||
# be authenticated), use @link #getsymval getsymval('{daemon_port}') @endlink.
|
|
||||||
# The <code>{daemon_port}</code> macro must be enabled in sendmail.cf
|
|
||||||
# <pre>
|
|
||||||
# O Milter.macros.connect=j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}
|
|
||||||
# </pre>
|
|
||||||
# or sendmail.mc
|
|
||||||
# <pre>
|
|
||||||
# define(`confMILTER_MACROS_CONNECT', ``j, _, {daemon_name}, {daemon_port}, {if_name}, {if_addr}'')dnl
|
|
||||||
# </pre>
|
|
||||||
# @param hostname the PTR name or bracketed IP of the SMTP client
|
|
||||||
# @param family <code>socket.AF_INET</code>, <code>socket.AF_INET6</code>,
|
|
||||||
# or <code>socket.AF_UNIX</code>
|
|
||||||
# @param hostaddr a tuple or string with peer IP or socketname
|
|
||||||
@nocallback
|
|
||||||
def connect(self,hostname,family,hostaddr): return CONTINUE
|
|
||||||
## Called when the SMTP client says HELO.
|
|
||||||
# Returning REJECT prevents progress until a valid HELO is provided;
|
|
||||||
# this almost always results in terminating the connection.
|
|
||||||
@nocallback
|
|
||||||
def hello(self,hostname): return CONTINUE
|
|
||||||
## Called with bytes by default global envfrom callback.
|
|
||||||
# @since 1.0.5
|
|
||||||
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
|
|
||||||
# to pass bytes to @link #header the header callback @endlink instead,
|
|
||||||
# or trap utf-8 conversion exception, etc.
|
|
||||||
def envfrom_bytes(self,*b):
|
|
||||||
try:
|
|
||||||
e = getattr(self.envfrom,'error_strategy','surrogateescape')
|
|
||||||
if e == 'bytes':
|
|
||||||
#self.envfrom_bytes = self.envfrom
|
|
||||||
return self.envfrom(*b)
|
|
||||||
s = [v.decode(encoding='utf-8',errors=e) for v in b]
|
|
||||||
except UnicodeDecodeError: s = b
|
|
||||||
return self.envfrom(s[0],*s[1:])
|
|
||||||
## Called when the SMTP client says MAIL FROM. Called by the
|
|
||||||
# <a href="milter_api/xxfi_envfrom.html">
|
|
||||||
# xxfi_envfrom</a> callback.
|
|
||||||
# Returning REJECT rejects the message, but not the connection.
|
|
||||||
# The sender is the "envelope" from as defined by
|
|
||||||
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
|
||||||
# For the From: header (author) defined in
|
|
||||||
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
|
||||||
# see @link #header the header callback @endlink.
|
|
||||||
@nocallback
|
|
||||||
def envfrom(self,f,*s): return CONTINUE
|
|
||||||
## Called with bytes by default global envrcpt callback.
|
|
||||||
# @since 1.0.5
|
|
||||||
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
|
|
||||||
# to pass bytes to @link #header the header callback @endlink instead,
|
|
||||||
# or trap utf-8 conversion exception, etc.
|
|
||||||
def envrcpt_bytes(self,*b):
|
|
||||||
try:
|
|
||||||
e = getattr(self.envrcpt,'error_strategy','surrogateescape')
|
|
||||||
if e == 'bytes':
|
|
||||||
#self.envrcpt_bytes = self.envrcpt
|
|
||||||
return self.envrcpt(*b)
|
|
||||||
s = [v.decode(encoding='utf-8',errors=e) for v in b]
|
|
||||||
except UnicodeDecodeError: s = b
|
|
||||||
return self.envrcpt(s[0],*s[1:])
|
|
||||||
## Called when the SMTP client says RCPT TO. Called by the
|
|
||||||
# <a href="milter_api/xxfi_envrcpt.html">
|
|
||||||
# xxfi_envrcpt</a> callback.
|
|
||||||
# Returning REJECT rejects the current recipient, not the entire message.
|
|
||||||
# The recipient is the "envelope" recipient as defined by
|
|
||||||
# <a href="http://tools.ietf.org/html/rfc5321">RFC 5321</a>.
|
|
||||||
# For recipients defined in
|
|
||||||
# <a href="http://tools.ietf.org/html/rfc5322">RFC 5322</a>,
|
|
||||||
# for example To: or Cc:, see @link #header the header callback @endlink.
|
|
||||||
@nocallback
|
|
||||||
def envrcpt(self,to,*str): return CONTINUE
|
|
||||||
## Called when the SMTP client says DATA.
|
|
||||||
# Returning REJECT rejects the message without wasting bandwidth
|
|
||||||
# on the unwanted message.
|
|
||||||
# @since 0.9.2
|
|
||||||
@nocallback
|
|
||||||
def data(self): return CONTINUE
|
|
||||||
## Called with bytes by default global header callback.
|
|
||||||
# @param fld name decoded as ascii
|
|
||||||
# @param val field value as bytes
|
|
||||||
# @since 1.0.5
|
|
||||||
# Converts from utf-8 to unicode with surrogate escape. Can be overriden
|
|
||||||
# to pass bytes to @link #header the header callback @endlink instead,
|
|
||||||
# e.g. by assignment:
|
|
||||||
# <pre>
|
|
||||||
# mymilter.header_bytes = mymilter.header
|
|
||||||
# </pre>
|
|
||||||
# The <code>@decode('bytes')</code> decorator will also do this.
|
|
||||||
#
|
|
||||||
def header_bytes(self,fld,val):
|
|
||||||
try:
|
|
||||||
e = getattr(self.header,'error_strategy','surrogateescape')
|
|
||||||
if e == 'bytes':
|
|
||||||
self.header_bytes = self.header
|
|
||||||
return self.header(fld,val)
|
|
||||||
s = val.decode(encoding='utf-8',errors=e)
|
|
||||||
except UnicodeDecodeError: s = val
|
|
||||||
return self.header(fld,s)
|
|
||||||
## Called for each header field in the message body.
|
|
||||||
# @param field name decoded as ascii
|
|
||||||
# @param value field value decoded as utf-8 on python3
|
|
||||||
@nocallback
|
|
||||||
def header(self,field,value): return CONTINUE
|
|
||||||
## Called at the blank line that terminates the header fields.
|
|
||||||
@nocallback
|
|
||||||
def eoh(self): return CONTINUE
|
|
||||||
## Called to supply the body of the message to the Milter by chunks.
|
|
||||||
# @param blk a block of message bytes
|
|
||||||
@nocallback
|
|
||||||
def body(self,blk): return CONTINUE
|
|
||||||
## Called when the SMTP client issues an unknown command.
|
|
||||||
# @param cmd the unknown command
|
|
||||||
# @since 0.9.2
|
|
||||||
@nocallback
|
|
||||||
def unknown(self,cmd): return CONTINUE
|
|
||||||
## Called at the end of the message body.
|
|
||||||
# Most of the message manipulation actions can only take place from
|
|
||||||
# the eom callback.
|
|
||||||
def eom(self): return CONTINUE
|
|
||||||
## Called when the connection is abnormally terminated.
|
|
||||||
# The close callback is still called also.
|
|
||||||
def abort(self): return CONTINUE
|
|
||||||
## Called when the connection is closed.
|
|
||||||
def close(self): return CONTINUE
|
|
||||||
|
|
||||||
## Return mask of SMFIP_N* protocol option bits to clear for this class
|
|
||||||
# The @@nocallback and @@noreply decorators set the
|
|
||||||
# <code>milter_protocol</code> function attribute to the protocol mask bit to
|
|
||||||
# pass to libmilter, causing that callback or its reply to be skipped.
|
|
||||||
# Overriding a method creates a new function object, so that
|
|
||||||
# <code>milter_protocol</code> defaults to 0.
|
|
||||||
# Libmilter passes the protocol bits that the current MTA knows
|
|
||||||
# how to skip. We clear the ones we don't want to skip.
|
|
||||||
# The negation is somewhat mind bending, but it is simple.
|
|
||||||
# @since 0.9.2
|
|
||||||
@classmethod
|
|
||||||
def protocol_mask(klass):
|
|
||||||
try:
|
|
||||||
return klass._protocol_mask
|
|
||||||
except AttributeError:
|
|
||||||
p = P_RCPT_REJ | P_HDR_LEADSPC # turn these new features off by default
|
|
||||||
for func,(nr,nc) in OPTIONAL_CALLBACKS.items():
|
|
||||||
func = getattr(klass,func)
|
|
||||||
ca = getattr(func,'milter_protocol',0)
|
|
||||||
#print(func,hex(nr),hex(nc),hex(ca))
|
|
||||||
p |= (nr|nc) & ~ca
|
|
||||||
klass._protocol_mask = p
|
|
||||||
return p
|
|
||||||
|
|
||||||
## Negotiate milter protocol options. Called by the
|
|
||||||
# <a href="milter_api/xxfi_negotiate.html">
|
|
||||||
# xffi_negotiate</a> callback. This is an advanced callback,
|
|
||||||
# do not override unless you know what you are doing. Most
|
|
||||||
# negotiation can be done simply by using the supplied
|
|
||||||
# class and function decorators.
|
|
||||||
# Options are passed as
|
|
||||||
# a list of 4 32-bit ints which can be modified and are passed
|
|
||||||
# back to libmilter on return.
|
|
||||||
# Default negotiation sets P_NO* and P_NR* for callbacks
|
|
||||||
# marked @@nocallback and @@noreply respectively, leaves all
|
|
||||||
# actions enabled, and enables Milter.SKIP. The @@enable_protocols
|
|
||||||
# class decorator can customize which protocol steps are implemented.
|
|
||||||
# @param opts a modifiable list of 4 ints with negotiated options
|
|
||||||
# @since 0.9.2
|
|
||||||
def negotiate(self,opts):
|
|
||||||
try:
|
|
||||||
self._actions,p,f1,f2 = opts
|
|
||||||
for func,stage in MACRO_CALLBACKS.items():
|
|
||||||
func = getattr(self,func)
|
|
||||||
syms = getattr(func,'_symlist',None)
|
|
||||||
if syms is not None:
|
|
||||||
self.setsymlist(stage,*syms)
|
|
||||||
opts[1] = self._protocol = p & ~self.protocol_mask()
|
|
||||||
opts[2] = 0
|
|
||||||
opts[3] = 0
|
|
||||||
#self.log("Negotiated:",opts)
|
|
||||||
except Exception as x:
|
|
||||||
# don't change anything if something went wrong
|
|
||||||
return ALL_OPTS
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
# Milter methods which can be invoked from most callbacks
|
|
||||||
|
|
||||||
## Return the value of an MTA macro. Sendmail macro names
|
|
||||||
# are either single chars (e.g. "j") or multiple chars enclosed
|
|
||||||
# in braces (e.g. "{auth_type}"). Macro names are MTA dependent.
|
|
||||||
# See <a href="milter_api/smfi_getsymval.html">
|
|
||||||
# smfi_getsymval</a> for default sendmail macros.
|
|
||||||
# @param sym the macro name
|
|
||||||
def getsymval(self,sym):
|
|
||||||
return self._ctx.getsymval(sym)
|
|
||||||
|
|
||||||
## Set the SMTP reply code and message.
|
|
||||||
# If the MTA does not support setmlreply, then only the
|
|
||||||
# first msg line is used. Any '%%' in a message line
|
|
||||||
# must be doubled, or libmilter will silently ignore the setreply.
|
|
||||||
# Beginning with 0.9.6, we test for that case and throw ValueError to avoid
|
|
||||||
# head scratching. What will <i>really</i> irritate you, however,
|
|
||||||
# is that if you carefully double any '%%', your message will be
|
|
||||||
# sent - but with the '%%' still doubled!
|
|
||||||
# See <a href="milter_api/smfi_setreply.html">
|
|
||||||
# smfi_setreply</a> for more information.
|
|
||||||
# @param rcode The three-digit (RFC 821/2821) SMTP reply code as a string.
|
|
||||||
# rcode cannot be None, and <b>must be a valid 4XX or 5XX reply code</b>.
|
|
||||||
# @param xcode The extended (RFC 1893/2034) reply code. If xcode is None,
|
|
||||||
# no extended code is used. Otherwise, xcode must conform to RFC 1893/2034.
|
|
||||||
# @param msg The text part of the SMTP reply. If msg is None,
|
|
||||||
# an empty message is used.
|
|
||||||
# @param ml Optional additional message lines.
|
|
||||||
def setreply(self,rcode,xcode=None,msg=None,*ml):
|
|
||||||
for m in (msg,)+ml:
|
|
||||||
if 1 in [len(s)&1 for s in R.findall(m)]:
|
|
||||||
raise ValueError("'%' must be doubled: "+m)
|
|
||||||
return self._ctx.setreply(rcode,xcode,msg,*ml)
|
|
||||||
|
|
||||||
## Tell the MTA which macro names will be used.
|
|
||||||
# This information can reduce the size of messages received from sendmail,
|
|
||||||
# and hence could reduce bandwidth between sendmail and your milter where
|
|
||||||
# that is a factor. The <code>Milter.SETSYMLIST</code> action flag must be
|
|
||||||
# set. The protocol stages are M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT,
|
|
||||||
# M_DATA, M_EOM, M_EOH.
|
|
||||||
#
|
|
||||||
# May only be called from negotiate callback. Hence, this is an advanced
|
|
||||||
# feature. Use the @@symlist function decorator to conviently set
|
|
||||||
# the macros used by a callback.
|
|
||||||
# @since 0.9.8, previous version was misspelled!
|
|
||||||
# @param stage the protocol stage to set to macro list for,
|
|
||||||
# one of the M_* constants defined in Milter
|
|
||||||
# @param macros space separated and/or lists of strings
|
|
||||||
def setsymlist(self,stage,*macros):
|
|
||||||
if not self._actions & SETSYMLIST: raise DisabledAction("SETSYMLIST")
|
|
||||||
if len(macros) > 5:
|
|
||||||
raise ValueError('setsymlist limited to 5 macros by MTA')
|
|
||||||
a = []
|
|
||||||
for m in macros:
|
|
||||||
try:
|
|
||||||
m = m.encode('utf8')
|
|
||||||
except: pass
|
|
||||||
try:
|
|
||||||
m = m.split(b' ')
|
|
||||||
a += m
|
|
||||||
except: pass
|
|
||||||
return self._ctx.setsymlist(stage,b' '.join(a))
|
|
||||||
|
|
||||||
# Milter methods which can only be called from eom callback.
|
|
||||||
|
|
||||||
## Add a mail header field.
|
|
||||||
# Calls <a href="milter_api/smfi_addheader.html">
|
|
||||||
# smfi_addheader</a>.
|
|
||||||
# The <code>Milter.ADDHDRS</code> action flag must be set.
|
|
||||||
#
|
|
||||||
# May be called from eom callback only.
|
|
||||||
# @param field the header field name
|
|
||||||
# @param value the header field value
|
|
||||||
# @param idx header field index from the top of the message to insert at
|
|
||||||
# @throws DisabledAction if ADDHDRS is not enabled
|
|
||||||
def addheader(self,field,value,idx=-1):
|
|
||||||
if not self._actions & ADDHDRS: raise DisabledAction("ADDHDRS")
|
|
||||||
return self._ctx.addheader(field,value,idx)
|
|
||||||
|
|
||||||
## Change the value of a mail header field.
|
|
||||||
# Calls <a href="milter_api/smfi_chgheader.html">
|
|
||||||
# smfi_chgheader</a>.
|
|
||||||
# The <code>Milter.CHGHDRS</code> action flag must be set.
|
|
||||||
#
|
|
||||||
# May be called from eom callback only.
|
|
||||||
# @param field the name of the field to change
|
|
||||||
# @param idx index of the field to change when there are multiple instances
|
|
||||||
# @param value the new value of the field
|
|
||||||
# @throws DisabledAction if CHGHDRS is not enabled
|
|
||||||
def chgheader(self,field,idx,value):
|
|
||||||
if not self._actions & CHGHDRS: raise DisabledAction("CHGHDRS")
|
|
||||||
return self._ctx.chgheader(field,idx,value)
|
|
||||||
|
|
||||||
## Add a recipient to the message.
|
|
||||||
# Calls <a href="milter_api/smfi_addrcpt.html">
|
|
||||||
# smfi_addrcpt</a>.
|
|
||||||
# If no corresponding mail header is added, this is like a Bcc.
|
|
||||||
# The syntax of the recipient is the same as used in the SMTP
|
|
||||||
# RCPT TO command (and as delivered to the envrcpt callback), for example
|
|
||||||
# "self.addrcpt('<foo@example.com>')".
|
|
||||||
# The <code>Milter.ADDRCPT</code> action flag must be set.
|
|
||||||
# If the optional <code>params</code> argument is used, then
|
|
||||||
# the <code>Milter.ADDRCPT_PAR</code> action flag must be set.
|
|
||||||
#
|
|
||||||
# May be called from eom callback only.
|
|
||||||
# @param rcpt the message recipient
|
|
||||||
# @param params an optional list of ESMTP parameters
|
|
||||||
# @throws DisabledAction if ADDRCPT or ADDRCPT_PAR is not enabled
|
|
||||||
def addrcpt(self,rcpt,params=None):
|
|
||||||
if not self._actions & ADDRCPT: raise DisabledAction("ADDRCPT")
|
|
||||||
if params and not self._actions & ADDRCPT_PAR:
|
|
||||||
raise DisabledAction("ADDRCPT_PAR")
|
|
||||||
return self._ctx.addrcpt(rcpt,params)
|
|
||||||
## Delete a recipient from the message.
|
|
||||||
# Calls <a href="milter_api/smfi_delrcpt.html">
|
|
||||||
# smfi_delrcpt</a>.
|
|
||||||
# The recipient should match one passed to the envrcpt callback.
|
|
||||||
# The <code>Milter.DELRCPT</code> action flag must be set.
|
|
||||||
#
|
|
||||||
# May be called from eom callback only.
|
|
||||||
# @param rcpt the message recipient to delete
|
|
||||||
# @throws DisabledAction if DELRCPT is not enabled
|
|
||||||
def delrcpt(self,rcpt):
|
|
||||||
if not self._actions & DELRCPT: raise DisabledAction("DELRCPT")
|
|
||||||
return self._ctx.delrcpt(rcpt)
|
|
||||||
|
|
||||||
## Replace the message body.
|
|
||||||
# Calls <a href="milter_api/smfi_replacebody.html">
|
|
||||||
# smfi_replacebody</a>.
|
|
||||||
# The entire message body must be replaced.
|
|
||||||
# Call repeatedly with blocks of data until the entire body is transferred.
|
|
||||||
# The <code>Milter.MODBODY</code> action flag must be set.
|
|
||||||
#
|
|
||||||
# May be called from eom callback only.
|
|
||||||
# @param body a chunk of body data
|
|
||||||
# @throws DisabledAction if MODBODY is not enabled
|
|
||||||
def replacebody(self,body):
|
|
||||||
if not self._actions & MODBODY: raise DisabledAction("MODBODY")
|
|
||||||
return self._ctx.replacebody(body)
|
|
||||||
|
|
||||||
## Change the SMTP envelope sender address.
|
|
||||||
# Calls <a href="milter_api/smfi_chgfrom.html">
|
|
||||||
# smfi_chgfrom</a>.
|
|
||||||
# The syntax of the sender is that same as used in the SMTP
|
|
||||||
# MAIL FROM command (and as delivered to the envfrom callback),
|
|
||||||
# for example <code>self.chgfrom('<bar@example.com>')</code>.
|
|
||||||
# The <code>Milter.CHGFROM</code> action flag must be set.
|
|
||||||
#
|
|
||||||
# May be called from eom callback only.
|
|
||||||
# @since 0.9.1
|
|
||||||
# @param sender the new sender address
|
|
||||||
# @param params an optional list of ESMTP parameters
|
|
||||||
# @throws DisabledAction if CHGFROM is not enabled
|
|
||||||
def chgfrom(self,sender,params=None):
|
|
||||||
if not self._actions & CHGFROM: raise DisabledAction("CHGFROM")
|
|
||||||
return self._ctx.chgfrom(sender,params)
|
|
||||||
|
|
||||||
## Quarantine the message.
|
|
||||||
# Calls <a href="milter_api/smfi_quarantine.html">
|
|
||||||
# smfi_quarantine</a>.
|
|
||||||
# When quarantined, a message goes into the mailq as if to be delivered,
|
|
||||||
# but delivery is deferred until the message is unquarantined.
|
|
||||||
# The <code>Milter.QUARANTINE</code> action flag must be set.
|
|
||||||
#
|
|
||||||
# May be called from eom callback only.
|
|
||||||
# @param reason a string describing the reason for quarantine
|
|
||||||
# @throws DisabledAction if QUARANTINE is not enabled
|
|
||||||
def quarantine(self,reason):
|
|
||||||
if not self._actions & QUARANTINE: raise DisabledAction("QUARANTINE")
|
|
||||||
return self._ctx.quarantine(reason)
|
|
||||||
|
|
||||||
## Tell the MTA to wait a bit longer.
|
|
||||||
# Calls <a href="milter_api/smfi_progress.html">
|
|
||||||
# smfi_progress</a>.
|
|
||||||
# Resets timeouts in the MTA that detect a "hung" milter.
|
|
||||||
def progress(self):
|
|
||||||
return self._ctx.progress()
|
|
||||||
|
|
||||||
## A logging but otherwise do nothing Milter base class.
|
|
||||||
# This is included for compatibility with previous versions of pymilter.
|
|
||||||
# The logging callbacks are marked @@noreply.
|
|
||||||
class Milter(Base):
|
|
||||||
"A simple class interface to the milter module."
|
|
||||||
|
|
||||||
## Provide simple logging to sys.stdout
|
|
||||||
def log(self,*msg):
|
|
||||||
print('Milter:',end=None)
|
|
||||||
for i in msg: print(i,end=None)
|
|
||||||
print()
|
|
||||||
|
|
||||||
@noreply
|
|
||||||
def connect(self,hostname,family,hostaddr):
|
|
||||||
"Called for each connection to sendmail."
|
|
||||||
self.log("connect from %s at %s" % (hostname,hostaddr))
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
@noreply
|
|
||||||
def hello(self,hostname):
|
|
||||||
"Called after the HELO command."
|
|
||||||
self.log("hello from %s" % hostname)
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
@noreply
|
|
||||||
def envfrom(self,f,*str):
|
|
||||||
"""Called to begin each message.
|
|
||||||
f -> string message sender
|
|
||||||
str -> tuple additional ESMTP parameters
|
|
||||||
"""
|
|
||||||
self.log("mail from",f,str)
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
@noreply
|
|
||||||
def envrcpt(self,to,*str):
|
|
||||||
"Called for each message recipient."
|
|
||||||
self.log("rcpt to",to,str)
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
@noreply
|
|
||||||
def header(self,field,value):
|
|
||||||
"Called for each message header."
|
|
||||||
self.log("%s: %s" % (field,value))
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
@noreply
|
|
||||||
def eoh(self):
|
|
||||||
"Called after all headers are processed."
|
|
||||||
self.log("eoh")
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
def eom(self):
|
|
||||||
"Called at the end of message."
|
|
||||||
self.log("eom")
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
def abort(self):
|
|
||||||
"Called if the connection is terminated abnormally."
|
|
||||||
self.log("abort")
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"Called at the end of connection, even if aborted."
|
|
||||||
self.log("close")
|
|
||||||
return CONTINUE
|
|
||||||
|
|
||||||
## The milter connection factory
|
|
||||||
# This factory method is called for each connection to create the
|
|
||||||
# python object that tracks the connection. It should return
|
|
||||||
# an object derived from Milter.Base.
|
|
||||||
#
|
|
||||||
# Note that since python is dynamic, this variable can be changed while
|
|
||||||
# the milter is running: for instance, to a new subclass based on a
|
|
||||||
# change in configuration.
|
|
||||||
factory = Milter
|
|
||||||
|
|
||||||
## @private
|
|
||||||
# @brief Connect context to connection instance and return enabled callbacks.
|
|
||||||
def negotiate_callback(ctx,opts):
|
|
||||||
m = factory()
|
|
||||||
m._setctx(ctx)
|
|
||||||
return m.negotiate(opts)
|
|
||||||
|
|
||||||
## @private
|
|
||||||
# @brief Connect context if needed and invoke connect method.
|
|
||||||
def connect_callback(ctx,hostname,family,hostaddr,nr_mask=P_NR_CONN):
|
|
||||||
m = ctx.getpriv()
|
|
||||||
if not m:
|
|
||||||
# If not already created (because the current MTA doesn't support
|
|
||||||
# xmfi_negotiate), create the connection object.
|
|
||||||
m = factory()
|
|
||||||
m._setctx(ctx)
|
|
||||||
return m.connect(hostname,family,hostaddr)
|
|
||||||
|
|
||||||
## @private
|
|
||||||
# @brief Disconnect milterContext and call close method.
|
|
||||||
def close_callback(ctx):
|
|
||||||
m = ctx.getpriv()
|
|
||||||
if not m: return CONTINUE
|
|
||||||
try:
|
|
||||||
rc = m.close()
|
|
||||||
finally:
|
|
||||||
m._setctx(None) # release milterContext
|
|
||||||
return rc
|
|
||||||
|
|
||||||
## Convert ESMTP parameters with values to a keyword dictionary.
|
|
||||||
# @deprecated You probably want Milter.param2dict instead.
|
|
||||||
def dictfromlist(args):
|
|
||||||
"Convert ESMTP parms with values to keyword dictionary."
|
|
||||||
kw = {}
|
|
||||||
for s in args:
|
|
||||||
pos = s.find('=')
|
|
||||||
if pos > 0:
|
|
||||||
kw[s[:pos].upper()] = s[pos+1:]
|
|
||||||
return kw
|
|
||||||
|
|
||||||
## Convert ESMTP parm list to keyword dictionary.
|
|
||||||
# Params with no value are set to None in the dictionary.
|
|
||||||
# @since 0.9.3
|
|
||||||
# @param str list of param strings of the form "NAME" or "NAME=VALUE"
|
|
||||||
# @return a dictionary of ESMTP param names and values
|
|
||||||
def param2dict(str):
|
|
||||||
"Convert ESMTP parm list to keyword dictionary."
|
|
||||||
pairs = [x.split('=',1) for x in str]
|
|
||||||
for e in pairs:
|
|
||||||
if len(e) < 2: e.append(None)
|
|
||||||
return dict([(k.upper(),v) for k,v in pairs])
|
|
||||||
|
|
||||||
def envcallback(c,args):
|
|
||||||
"""Call function c with ESMTP parms converted to keyword parameters.
|
|
||||||
Can be used in the envfrom and/or envrcpt callbacks to process
|
|
||||||
ESMTP parameters as python keyword parameters."""
|
|
||||||
kw = {}
|
|
||||||
pargs = [args[0]]
|
|
||||||
for s in args[1:]:
|
|
||||||
pos = s.find('=')
|
|
||||||
if pos > 0:
|
|
||||||
kw[s[:pos].upper()] = s[pos+1:]
|
|
||||||
else:
|
|
||||||
pargs.append(s)
|
|
||||||
return c(*pargs,**kw)
|
|
||||||
|
|
||||||
## Run the %milter.
|
|
||||||
# @param name the name of the %milter known to the MTA
|
|
||||||
# @param socketname the socket to be passed to milter.setconn()
|
|
||||||
# @param timeout the time in secs the MTA should wait for a response before
|
|
||||||
# considering this %milter dead
|
|
||||||
def runmilter(name,socketname,timeout = 0,rmsock=True):
|
|
||||||
|
|
||||||
# The default flags set include everything
|
|
||||||
# milter.set_flags(milter.ADDHDRS)
|
|
||||||
milter.set_connect_callback(connect_callback)
|
|
||||||
milter.set_helo_callback(lambda ctx, host: ctx.getpriv().hello(host))
|
|
||||||
# For envfrom and envrcpt, we would like to convert ESMTP parms to keyword
|
|
||||||
# parms, but then all existing users would have to include **kw to accept
|
|
||||||
# arbitrary keywords without crashing. We do provide envcallback and
|
|
||||||
# dictfromlist to make parsing the ESMTP args convenient.
|
|
||||||
if sys.version < '3.0.0':
|
|
||||||
milter.set_envfrom_callback(lambda ctx,*s: ctx.getpriv().envfrom(*s))
|
|
||||||
milter.set_envrcpt_callback(lambda ctx,*s: ctx.getpriv().envrcpt(*s))
|
|
||||||
milter.set_header_callback(lambda ctx,f,v: ctx.getpriv().header(f,v))
|
|
||||||
else:
|
|
||||||
milter.set_envfrom_callback(lambda ctx,*b: ctx.getpriv().envfrom_bytes(*b))
|
|
||||||
milter.set_envrcpt_callback(lambda ctx,*b: ctx.getpriv().envrcpt_bytes(*b))
|
|
||||||
milter.set_header_callback(lambda ctx,f,v: ctx.getpriv().header_bytes(f,v))
|
|
||||||
milter.set_eoh_callback(lambda ctx: ctx.getpriv().eoh())
|
|
||||||
milter.set_body_callback(lambda ctx,chunk: ctx.getpriv().body(chunk))
|
|
||||||
milter.set_eom_callback(lambda ctx: ctx.getpriv().eom())
|
|
||||||
milter.set_abort_callback(lambda ctx: ctx.getpriv().abort())
|
|
||||||
milter.set_close_callback(close_callback)
|
|
||||||
|
|
||||||
milter.setconn(socketname)
|
|
||||||
if timeout > 0: milter.settimeout(timeout)
|
|
||||||
# disable negotiate callback if runtime version < (1,0,1)
|
|
||||||
ncb = negotiate_callback
|
|
||||||
if milter.getversion() < (1,0,1):
|
|
||||||
ncb = None
|
|
||||||
# The name *must* match the X line in sendmail.cf (supposedly)
|
|
||||||
milter.register(name,
|
|
||||||
data=lambda ctx: ctx.getpriv().data(),
|
|
||||||
unknown=lambda ctx,cmd: ctx.getpriv().unknown(cmd),
|
|
||||||
negotiate=ncb
|
|
||||||
)
|
|
||||||
|
|
||||||
# We remove the socket here by default on the assumption that you will be
|
|
||||||
# starting this filter before sendmail. If sendmail is not running and the
|
|
||||||
# socket already exists, libmilter will throw a warning. If sendmail is
|
|
||||||
# running, this is still safe if there are no messages currently being
|
|
||||||
# processed. It's safer to shutdown sendmail, kill the filter process,
|
|
||||||
# restart the filter, and then restart sendmail.
|
|
||||||
milter.opensocket(rmsock)
|
|
||||||
start_seq = _seq
|
|
||||||
try:
|
|
||||||
milter.main()
|
|
||||||
except milter.error:
|
|
||||||
if start_seq == _seq: raise # couldn't start
|
|
||||||
# milter has been running for a while, but now it can't start new threads
|
|
||||||
raise milter.error("out of thread resources")
|
|
||||||
|
|
||||||
__all__ = globals().copy()
|
|
||||||
for priv in ('os','milter','thread','factory','_seq','_seq_lock','__version__'):
|
|
||||||
del __all__[priv]
|
|
||||||
__all__ = __all__.keys()
|
|
||||||
|
|
||||||
## @example milter-template.py
|
|
||||||
## @example milter-nomix.py
|
|
||||||
#
|
|
||||||
-164
@@ -1,164 +0,0 @@
|
|||||||
# Email address list with expiration
|
|
||||||
#
|
|
||||||
# This class acts like a map. Entries with a value of None are persistent,
|
|
||||||
# but disappear after a time limit. This is useful for automatic whitelists
|
|
||||||
# and blacklists with expiration. The persistent store is a simple ascii
|
|
||||||
# file with sender and timestamp on each line. Entries can be appended
|
|
||||||
# to the store, and will be picked up the next time it is loaded.
|
|
||||||
#
|
|
||||||
# Entries with other values are not persistent. This is used to hold failed
|
|
||||||
# CBV results.
|
|
||||||
#
|
|
||||||
# $Log$
|
|
||||||
# Revision 1.9 2008/05/08 21:35:57 customdesigned
|
|
||||||
# Allow explicitly whitelisted email from banned_users.
|
|
||||||
#
|
|
||||||
# Revision 1.8 2007/09/03 16:18:45 customdesigned
|
|
||||||
# Delete unparseable timestamps when loading address cache. These have
|
|
||||||
# arisen because of failure to parse MAIL FROM properly. Will have to
|
|
||||||
# tighten up MAIL FROM parsing to match RFC.
|
|
||||||
#
|
|
||||||
# Revision 1.7 2007/01/25 22:47:26 customdesigned
|
|
||||||
# Persist blacklisting from delayed DSNs.
|
|
||||||
#
|
|
||||||
# Revision 1.6 2007/01/19 23:31:38 customdesigned
|
|
||||||
# Move parse_header to Milter.utils.
|
|
||||||
# Test case for delayed DSN parsing.
|
|
||||||
# Fix plock when source missing or cannot set owner/group.
|
|
||||||
#
|
|
||||||
# Revision 1.5 2007/01/11 19:59:40 customdesigned
|
|
||||||
# Purge old entries in auto_whitelist and send_dsn logs.
|
|
||||||
#
|
|
||||||
# Revision 1.4 2007/01/11 04:31:26 customdesigned
|
|
||||||
# Negative feedback for bad headers. Purge cache logs on startup.
|
|
||||||
#
|
|
||||||
# Revision 1.3 2007/01/08 23:20:54 customdesigned
|
|
||||||
# Get user feedback.
|
|
||||||
#
|
|
||||||
# Revision 1.2 2007/01/05 23:33:55 customdesigned
|
|
||||||
# Make blacklist an AddrCache
|
|
||||||
#
|
|
||||||
# Revision 1.1 2007/01/05 21:25:40 customdesigned
|
|
||||||
# Move AddrCache to Milter package.
|
|
||||||
#
|
|
||||||
|
|
||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
import time
|
|
||||||
from Milter.plock import PLock
|
|
||||||
|
|
||||||
class AddrCache(object):
|
|
||||||
time_format = '%Y%b%d %H:%M:%S %Z'
|
|
||||||
|
|
||||||
def __init__(self,renew=7,fname=None):
|
|
||||||
self.age = renew
|
|
||||||
self.cache = {}
|
|
||||||
self.fname = fname
|
|
||||||
|
|
||||||
def load(self,fname,age=0):
|
|
||||||
"Load address cache from persistent store."
|
|
||||||
if not age:
|
|
||||||
age = self.age
|
|
||||||
self.fname = fname
|
|
||||||
cache = {}
|
|
||||||
self.cache = cache
|
|
||||||
now = time.time()
|
|
||||||
lock = PLock(self.fname)
|
|
||||||
wfp = lock.lock()
|
|
||||||
changed = False
|
|
||||||
try:
|
|
||||||
too_old = now - age*24*60*60 # max age in days
|
|
||||||
try:
|
|
||||||
fp = open(self.fname)
|
|
||||||
except OSError:
|
|
||||||
fp = ()
|
|
||||||
for ln in fp:
|
|
||||||
try:
|
|
||||||
rcpt,ts = ln.strip().split(None,1)
|
|
||||||
try:
|
|
||||||
l = time.strptime(ts,AddrCache.time_format)
|
|
||||||
t = time.mktime(l)
|
|
||||||
if t < too_old:
|
|
||||||
changed = True
|
|
||||||
continue
|
|
||||||
cache[rcpt.lower()] = (t,None)
|
|
||||||
except: # unparsable timestamp - likely garbage
|
|
||||||
changed = True
|
|
||||||
continue
|
|
||||||
except: # manual entry (no timestamp)
|
|
||||||
cache[ln.strip().lower()] = (now,None)
|
|
||||||
wfp.write(ln)
|
|
||||||
if changed:
|
|
||||||
lock.commit(self.fname+'.old')
|
|
||||||
else:
|
|
||||||
lock.unlock()
|
|
||||||
except IOError:
|
|
||||||
lock.unlock()
|
|
||||||
|
|
||||||
def has_precise_key(self,sender):
|
|
||||||
"""True if precise sender is cached and has not expired. Don't
|
|
||||||
try looking up wildcard entries.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
lsender = sender and sender.lower()
|
|
||||||
ts,res = self.cache[lsender]
|
|
||||||
too_old = time.time() - self.age*24*60*60 # max age in days
|
|
||||||
if not ts or ts > too_old:
|
|
||||||
return True
|
|
||||||
del self.cache[lsender]
|
|
||||||
except KeyError: pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
def has_key(self,sender):
|
|
||||||
"True if sender is cached and has not expired."
|
|
||||||
if self.has_precise_key(sender):
|
|
||||||
return True
|
|
||||||
try:
|
|
||||||
user,host = sender.split('@',1)
|
|
||||||
return self.has_precise_key(host)
|
|
||||||
except: pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
__contains__ = has_key
|
|
||||||
|
|
||||||
def __getitem__(self,sender):
|
|
||||||
try:
|
|
||||||
lsender = sender.lower()
|
|
||||||
ts,res = self.cache[lsender]
|
|
||||||
too_old = time.time() - self.age*24*60*60 # max age in days
|
|
||||||
if not ts or ts > too_old:
|
|
||||||
return res
|
|
||||||
del self.cache[lsender]
|
|
||||||
raise KeyError(sender)
|
|
||||||
except KeyError as x:
|
|
||||||
try:
|
|
||||||
user,host = sender.split('@',1)
|
|
||||||
return self.__getitem__(host)
|
|
||||||
except ValueError:
|
|
||||||
raise x
|
|
||||||
|
|
||||||
def addperm(self,sender,res=None):
|
|
||||||
"Add a permanent sender."
|
|
||||||
lsender = sender.lower()
|
|
||||||
if self.has_key(lsender):
|
|
||||||
ts,res = self.cache[lsender]
|
|
||||||
if not ts: return # already permanent
|
|
||||||
self.cache[lsender] = (None,res)
|
|
||||||
if not res:
|
|
||||||
with open(self.fname,'a') as fp:
|
|
||||||
print(sender,file=fp)
|
|
||||||
|
|
||||||
def __setitem__(self,sender,res):
|
|
||||||
lsender = sender.lower()
|
|
||||||
now = time.time()
|
|
||||||
self.cache[lsender] = (now,res)
|
|
||||||
if not res and self.fname:
|
|
||||||
s = time.strftime(AddrCache.time_format,time.localtime(now))
|
|
||||||
with open(self.fname,'a') as fp:
|
|
||||||
print(sender,s,file=fp) # log refreshed senders
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.cache)
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
try:
|
|
||||||
from configparser import ConfigParser
|
|
||||||
except:
|
|
||||||
from ConfigParser import ConfigParser
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
class MilterConfigParser(ConfigParser):
|
|
||||||
|
|
||||||
def __init__(self,defaults={}):
|
|
||||||
ConfigParser.__init__(self)
|
|
||||||
self.defaults = defaults
|
|
||||||
|
|
||||||
# The defaults provided by ConfigParser show up in all sections,
|
|
||||||
# which screws up iterating over all options in a section.
|
|
||||||
# Worse, passing "defaults" with vars= overrides the config file!
|
|
||||||
# So we roll our own defaults.
|
|
||||||
def get(self,sect,opt,fallback=None,**kwds):
|
|
||||||
if not self.has_option(sect,opt) and not fallback and opt in self.defaults:
|
|
||||||
return self.defaults[opt]
|
|
||||||
return ConfigParser.get(self,sect,opt,fallback=fallback,**kwds)
|
|
||||||
|
|
||||||
def getlist(self,sect,opt):
|
|
||||||
if self.has_option(sect,opt):
|
|
||||||
return [q.strip() for q in self.get(sect,opt).split(',')]
|
|
||||||
return []
|
|
||||||
|
|
||||||
def getaddrset(self,sect,opt,dir=''):
|
|
||||||
if not self.has_option(sect,opt):
|
|
||||||
return {}
|
|
||||||
s = self.get(sect,opt)
|
|
||||||
d = {}
|
|
||||||
for q in s.split(','):
|
|
||||||
q = q.strip()
|
|
||||||
if q.startswith('file:'):
|
|
||||||
domain = q[5:].lower()
|
|
||||||
fname = os.path.join(dir,domain)
|
|
||||||
with open(fname,'r') as fp:
|
|
||||||
d[domain] = d.setdefault(domain,[]) + fp.read().split()
|
|
||||||
else:
|
|
||||||
user,domain = q.split('@')
|
|
||||||
d.setdefault(domain.lower(),[]).append(user)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def getaddrdict(self,sect,opt,dir=''):
|
|
||||||
if not self.has_option(sect,opt):
|
|
||||||
return {}
|
|
||||||
d = {}
|
|
||||||
for q in self.get(sect,opt).split(','):
|
|
||||||
q = q.strip()
|
|
||||||
if self.has_option(sect,q):
|
|
||||||
l = self.get(sect,q)
|
|
||||||
for addr in l.split(','):
|
|
||||||
addr = addr.strip()
|
|
||||||
if addr.startswith('file:'):
|
|
||||||
fname = os.path.join(dir,addr[5:])
|
|
||||||
with open(fname,'r') as fp:
|
|
||||||
for a in fp.read().split():
|
|
||||||
d[a] = q
|
|
||||||
else:
|
|
||||||
d[addr] = q
|
|
||||||
return d
|
|
||||||
|
|
||||||
def getdefault(self,sect,opt,default=None):
|
|
||||||
if self.has_option(sect,opt):
|
|
||||||
return self.get(sect,opt)
|
|
||||||
return default
|
|
||||||
|
|
||||||
def getintdefault(self,sect,opt,default=None):
|
|
||||||
if self.has_option(sect,opt):
|
|
||||||
return self.getint(sect,opt)
|
|
||||||
return default
|
|
||||||
-125
@@ -1,125 +0,0 @@
|
|||||||
## @package Milter.dns
|
|
||||||
# Provide a higher level interface to pydns.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
import DNS
|
|
||||||
from DNS import DNSError
|
|
||||||
|
|
||||||
MAX_CNAME = 10
|
|
||||||
|
|
||||||
## Lookup DNS records by label and RR type.
|
|
||||||
# The response can include records of other types that the DNS
|
|
||||||
# server thinks we might need.
|
|
||||||
# @param name the DNS label to lookup
|
|
||||||
# @param qtype the name of the DNS RR type to lookup
|
|
||||||
# @return a list of ((name,type),data) tuples
|
|
||||||
def DNSLookup(name, qtype):
|
|
||||||
try:
|
|
||||||
# To be thread safe, we create a fresh DnsRequest with
|
|
||||||
# each call. It would be more efficient to reuse
|
|
||||||
# a req object stored in a Session.
|
|
||||||
req = DNS.DnsRequest(name, qtype=qtype)
|
|
||||||
resp = req.req()
|
|
||||||
#resp.show()
|
|
||||||
# key k: ('wayforward.net', 'A'), value v
|
|
||||||
# FIXME: pydns returns AAAA RR as 16 byte binary string, but
|
|
||||||
# A RR as dotted quad. For consistency, this driver should
|
|
||||||
# return both as binary string.
|
|
||||||
return [((a['name'], a['typename']), a['data']) for a in resp.answers]
|
|
||||||
except IOError as x:
|
|
||||||
raise DNSError(str(x))
|
|
||||||
|
|
||||||
class Session(object):
|
|
||||||
"""A Session object has a simple cache with no TTL that is valid
|
|
||||||
for a single "session", for example an SMTP conversation."""
|
|
||||||
def __init__(self):
|
|
||||||
self.cache = {}
|
|
||||||
|
|
||||||
## Additional DNS RRs we can safely cache.
|
|
||||||
# We have to be careful which additional DNS RRs we cache. For
|
|
||||||
# instance, PTR records are controlled by the connecting IP, and they
|
|
||||||
# could poison our local cache with bogus A and MX records.
|
|
||||||
# Each entry is a tuple of (query_type,rr_type). So for instance,
|
|
||||||
# the entry ('MX','A') says it is safe (for milter purposes) to cache
|
|
||||||
# any 'A' RRs found in an 'MX' query.
|
|
||||||
SAFE2CACHE = frozenset((
|
|
||||||
('MX','MX'), ('MX','A'),
|
|
||||||
('CNAME','CNAME'), ('CNAME','A'),
|
|
||||||
('A','A'),
|
|
||||||
('AAAA','AAAA'),
|
|
||||||
('PTR','PTR'),
|
|
||||||
('NS','NS'), ('NS','A'),
|
|
||||||
('TXT','TXT'),
|
|
||||||
('SPF','SPF')
|
|
||||||
))
|
|
||||||
|
|
||||||
## Cached DNS lookup.
|
|
||||||
# @param name the DNS label to query
|
|
||||||
# @param qtype the query type, e.g. 'A'
|
|
||||||
# @param cnames tracks CNAMES already followed in recursive calls
|
|
||||||
def dns(self, name, qtype, cnames=None):
|
|
||||||
"""DNS query.
|
|
||||||
|
|
||||||
If the result is in cache, return that. Otherwise pull the
|
|
||||||
result from DNS, and cache ALL answers, so additional info
|
|
||||||
is available for further queries later.
|
|
||||||
|
|
||||||
CNAMEs are followed.
|
|
||||||
|
|
||||||
If there is no data, [] is returned.
|
|
||||||
|
|
||||||
pre: qtype in ['A', 'AAAA', 'MX', 'PTR', 'TXT', 'SPF']
|
|
||||||
post: isinstance(__return__, types.ListType)
|
|
||||||
"""
|
|
||||||
if name.endswith('.'): name = name[:-1]
|
|
||||||
if not reduce(lambda x,y:x and 0 < len(y) < 64, name.split('.'),True):
|
|
||||||
return [] # invalid DNS name (too long or empty)
|
|
||||||
name = name.lower()
|
|
||||||
result = self.cache.get( (name, qtype) )
|
|
||||||
cname = None
|
|
||||||
if result: return result
|
|
||||||
cnamek = (name,'CNAME')
|
|
||||||
cname = self.cache.get( cnamek )
|
|
||||||
|
|
||||||
if cname:
|
|
||||||
cname = cname[0]
|
|
||||||
else:
|
|
||||||
safe2cache = Session.SAFE2CACHE
|
|
||||||
for k, v in DNSLookup(name, qtype):
|
|
||||||
if k == cnamek:
|
|
||||||
cname = v
|
|
||||||
if k[1] == 'CNAME' or (qtype,k[1]) in safe2cache:
|
|
||||||
self.cache.setdefault(k, []).append(v)
|
|
||||||
result = self.cache.get( (name, qtype), [])
|
|
||||||
if not result and cname:
|
|
||||||
if not cnames:
|
|
||||||
cnames = {}
|
|
||||||
elif len(cnames) >= MAX_CNAME:
|
|
||||||
#return result # if too many == NX_DOMAIN
|
|
||||||
raise DNSError('Length of CNAME chain exceeds %d' % MAX_CNAME)
|
|
||||||
cnames[name] = cname
|
|
||||||
if cname.lower().rstrip('.') in cnames:
|
|
||||||
raise DNSError('CNAME loop')
|
|
||||||
result = self.dns(cname, qtype, cnames=cnames)
|
|
||||||
if result:
|
|
||||||
self.cache[(name,qtype)] = result
|
|
||||||
return result
|
|
||||||
|
|
||||||
def dns_txt(self, domainname, enc='ascii'):
|
|
||||||
"Get a list of TXT records for a domain name."
|
|
||||||
if domainname:
|
|
||||||
try:
|
|
||||||
return [''.join(s.decode(enc) for s in a)
|
|
||||||
for a in self.dns(domainname, 'TXT')]
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
raise DNSError('Non-ascii character in SPF TXT record.')
|
|
||||||
return []
|
|
||||||
|
|
||||||
DNS.DiscoverNameServers()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
s = Session()
|
|
||||||
for n,t in zip(*[iter(sys.argv[1:])]*2):
|
|
||||||
print(n,t)
|
|
||||||
print(s.dns(n,t))
|
|
||||||
-239
@@ -1,239 +0,0 @@
|
|||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2005 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
|
|
||||||
# Send DSNs, do call back verification,
|
|
||||||
# and generate DSN messages from a template
|
|
||||||
# $Log$
|
|
||||||
# Revision 1.22 2011/03/18 20:41:31 customdesigned
|
|
||||||
# Python2.6 SMTP.close() fails when instance never connected.
|
|
||||||
#
|
|
||||||
# Revision 1.21 2011/03/03 05:11:58 customdesigned
|
|
||||||
# Release 0.9.4
|
|
||||||
#
|
|
||||||
# Revision 1.20 2010/10/11 00:29:47 customdesigned
|
|
||||||
# Handle multiple recipients. For CBV or auto whitelist of multiple emails.
|
|
||||||
#
|
|
||||||
# Revision 1.19 2009/07/02 19:41:12 customdesigned
|
|
||||||
# Handle @ in localpart.
|
|
||||||
#
|
|
||||||
# Revision 1.18 2009/06/10 18:01:59 customdesigned
|
|
||||||
# Doxygen updates
|
|
||||||
#
|
|
||||||
# Revision 1.17 2009/05/20 20:08:44 customdesigned
|
|
||||||
# Support non-DSN CBV (non-empty MAIL FROM)
|
|
||||||
#
|
|
||||||
# Revision 1.16 2007/09/25 01:24:59 customdesigned
|
|
||||||
# Allow arbitrary object, not just spf.query like, to provide data for create_msg
|
|
||||||
#
|
|
||||||
# Revision 1.15 2007/09/24 20:13:26 customdesigned
|
|
||||||
# Remove explicit spf dependency.
|
|
||||||
#
|
|
||||||
# Revision 1.14 2007/03/03 18:19:40 customdesigned
|
|
||||||
# Handle DNS error sending DSN.
|
|
||||||
#
|
|
||||||
# Revision 1.13 2007/01/04 18:01:11 customdesigned
|
|
||||||
# Do plain CBV when template missing.
|
|
||||||
#
|
|
||||||
# Revision 1.12 2006/07/26 16:37:35 customdesigned
|
|
||||||
# Support timeout.
|
|
||||||
#
|
|
||||||
# Revision 1.11 2006/06/21 21:07:11 customdesigned
|
|
||||||
# Include header fields in DSN template.
|
|
||||||
#
|
|
||||||
# Revision 1.10 2006/05/24 20:56:35 customdesigned
|
|
||||||
# Remove default templates. Scrub test.
|
|
||||||
#
|
|
||||||
## @package Milter.dsn
|
|
||||||
# Support DSNs and CallBackValidations (CBV).
|
|
||||||
#
|
|
||||||
# A Delivery Status Notification (bounce) is sent to the envelope
|
|
||||||
# sender (original MAIL FROM) with a null MAIL FROM (<>) to notify the
|
|
||||||
# original sender # of delays or problems with delivery. A Callback Validation
|
|
||||||
# starts the DSN process, but stops before issuing the DATA command. The
|
|
||||||
# purpose is to check whether the envelope recipient is accepted (and is
|
|
||||||
# therefore a valid email). The null MAIL FROM tells the remote
|
|
||||||
# MTA to never reply according to RFC2821 (but some braindead MTAs
|
|
||||||
# reply anyway, of course).
|
|
||||||
#
|
|
||||||
# Milters should cache CBV results and should avoid sending DSNs
|
|
||||||
# unless the sender is authenticated somehow (e.g. SPF Pass). However,
|
|
||||||
# when email is quarantined, and is not known to be a forgery, sending a DSN
|
|
||||||
# is better than silently disappearing, and a DSN is better than sending
|
|
||||||
# a normal message as notification - because MAIL FROM signing schemes
|
|
||||||
# can reject bounces of forged emails. Whatever you do, don't copy those
|
|
||||||
# assinine commercial filters that send a normal message to notify you
|
|
||||||
# that some virus is forging your email.
|
|
||||||
#
|
|
||||||
# <b>DSNs should *only* be sent to MAIL FROM addresses.</b> Never send
|
|
||||||
# a DSN or use a null MAIL FROM with an email address obtained from
|
|
||||||
# anywhere else.
|
|
||||||
#
|
|
||||||
from __future__ import print_function
|
|
||||||
import smtplib
|
|
||||||
import socket
|
|
||||||
try:
|
|
||||||
from email.message import Message
|
|
||||||
except:
|
|
||||||
from email.Message import Message
|
|
||||||
import Milter
|
|
||||||
import Milter.dns as dns
|
|
||||||
import time
|
|
||||||
|
|
||||||
## Send DSN.
|
|
||||||
# Try the published MX names in order, rejecting obviously bogus entries
|
|
||||||
# (like <code>localhost</code>).
|
|
||||||
# @param mailfrom the original sender we are notifying or validating
|
|
||||||
# @param receiver the HELO name of the MTA we are sending the DSN on behalf of.
|
|
||||||
# Be sure to send from an IP that matches the HELO.
|
|
||||||
# @param msg the DSN message in RFC2822 format, or None for CBV.
|
|
||||||
# @param timeout total seconds to wait for a response from an MX
|
|
||||||
# @param session Milter.dns.Session object from current incoming mail
|
|
||||||
# session to reuse its cache, or None to create a fresh one.
|
|
||||||
# @param ourfrom set to a valid email to send a normal notification from, or
|
|
||||||
# to validate emails not obtained from MAIL FROM.
|
|
||||||
# @return None on success or (status_code,msg) on failure.
|
|
||||||
def send_dsn(mailfrom,receiver,msg=None,timeout=600,session=None,ourfrom=''):
|
|
||||||
"""Send DSN. If msg is None, do callback verification.
|
|
||||||
Mailfrom is original sender we are sending DSN or CBV to.
|
|
||||||
Receiver is the MTA sending the DSN.
|
|
||||||
Return None for success or (code,msg) for failure."""
|
|
||||||
user,domain = mailfrom.rsplit('@',1)
|
|
||||||
if not session: session = dns.Session()
|
|
||||||
try:
|
|
||||||
mxlist = session.dns(domain,'MX')
|
|
||||||
except dns.DNSError:
|
|
||||||
return (450,'DNS Timeout: %s MX'%domain) # temp error
|
|
||||||
if not mxlist:
|
|
||||||
mxlist = (0,domain), # fallback to A record when no MX
|
|
||||||
else:
|
|
||||||
mxlist.sort()
|
|
||||||
smtp = smtplib.SMTP()
|
|
||||||
toolate = time.time() + timeout
|
|
||||||
for prior,host in mxlist:
|
|
||||||
try:
|
|
||||||
smtp.connect(host)
|
|
||||||
code,resp = smtp.helo(receiver)
|
|
||||||
# some wiley spammers have MX records that resolve to 127.0.0.1
|
|
||||||
a = resp.split()
|
|
||||||
if not a:
|
|
||||||
return (553,'MX for %s has no hostname in banner: %s' % (domain,host))
|
|
||||||
if a[0] == receiver:
|
|
||||||
return (553,'Fraudulent MX for %s: %s' % (domain,host))
|
|
||||||
if not (200 <= code <= 299):
|
|
||||||
raise smtplib.SMTPHeloError(code, resp)
|
|
||||||
if msg:
|
|
||||||
try:
|
|
||||||
smtp.sendmail('<%s>'%ourfrom,mailfrom,msg)
|
|
||||||
except smtplib.SMTPSenderRefused:
|
|
||||||
# does not accept DSN, try postmaster (at the risk of mail loops)
|
|
||||||
smtp.sendmail('<postmaster@%s>'%receiver,mailfrom,msg)
|
|
||||||
else: # CBV
|
|
||||||
code,resp = smtp.docmd('MAIL FROM: <%s>'%ourfrom)
|
|
||||||
if code != 250:
|
|
||||||
raise smtplib.SMTPSenderRefused(code, resp, '<%s>'%ourfrom)
|
|
||||||
if isinstance(mailfrom,basestring):
|
|
||||||
mailfrom = [mailfrom]
|
|
||||||
badrcpts = {}
|
|
||||||
for rcpt in mailfrom:
|
|
||||||
code,resp = smtp.rcpt(rcpt)
|
|
||||||
if code not in (250,251):
|
|
||||||
badrcpts[rcpt] = (code,resp)# permanent error
|
|
||||||
smtp.quit()
|
|
||||||
if len(badrcpts) == 1:
|
|
||||||
return badrcpts.values()[0] # permanent error
|
|
||||||
if badrcpts:
|
|
||||||
return badrcpts
|
|
||||||
return None # success
|
|
||||||
except smtplib.SMTPRecipientsRefused as x:
|
|
||||||
if len(x.recipients) == 1:
|
|
||||||
return x.recipients.values()[0] # permanent error
|
|
||||||
return x.recipients
|
|
||||||
except smtplib.SMTPSenderRefused as x:
|
|
||||||
return x.args[:2] # does not accept DSN
|
|
||||||
except smtplib.SMTPDataError as x:
|
|
||||||
return x.args # permanent error
|
|
||||||
except smtplib.SMTPException:
|
|
||||||
pass # any other error, try next MX
|
|
||||||
except socket.error:
|
|
||||||
pass # MX didn't accept connections, try next one
|
|
||||||
except socket.timeout:
|
|
||||||
pass # MX too slow, try next one
|
|
||||||
if hasattr(smtp,'sock'): smtp.close()
|
|
||||||
if time.time() > toolate:
|
|
||||||
return (450,'No MX response within %f minutes'%(timeout/60.0))
|
|
||||||
return (450,'No MX servers available') # temp error
|
|
||||||
|
|
||||||
class Vars: pass
|
|
||||||
|
|
||||||
# NOTE: Caller can pass an object to create_msg that in a typical milter
|
|
||||||
# collects things like heloname or sender anyway.
|
|
||||||
def create_msg(v,rcptlist=None,origmsg=None,template=None):
|
|
||||||
"""Create a DSN message from a template. Template must be '\n' separated.
|
|
||||||
v - an object whose attributes are used for substitutions. Must
|
|
||||||
have sender and receiver attributes at a minimum.
|
|
||||||
rcptlist - used to set v.rcpt if given
|
|
||||||
origmsg - used to set v.subject and v.spf_result if given
|
|
||||||
template - a '\n' separated string with python '%(name)s' substitutions.
|
|
||||||
"""
|
|
||||||
if not template:
|
|
||||||
return None
|
|
||||||
if hasattr(v,'perm_error'):
|
|
||||||
# likely to be an spf.query, try translating for backward compatibility
|
|
||||||
q = v
|
|
||||||
v = Vars()
|
|
||||||
try:
|
|
||||||
v.heloname = q.h
|
|
||||||
v.sender = q.s
|
|
||||||
v.connectip = q.i
|
|
||||||
v.receiver = q.r
|
|
||||||
v.sender_domain = q.o
|
|
||||||
v.result = q.result
|
|
||||||
v.perm_error = q.perm_error
|
|
||||||
except: v = q
|
|
||||||
if rcptlist:
|
|
||||||
v.rcpt = '\n\t'.join(rcptlist)
|
|
||||||
if origmsg:
|
|
||||||
try: v.subject = origmsg['Subject']
|
|
||||||
except: v.subject = '(none)'
|
|
||||||
try:
|
|
||||||
v.spf_result = origmsg['Received-SPF']
|
|
||||||
except: v.spf_result = None
|
|
||||||
|
|
||||||
msg = Message()
|
|
||||||
|
|
||||||
msg.add_header('X-Mailer','PyMilter-'+Milter.__version__)
|
|
||||||
msg.set_type('text/plain')
|
|
||||||
|
|
||||||
hdrs,body = template.split('\n\n',1)
|
|
||||||
for ln in hdrs.splitlines():
|
|
||||||
name,val = ln.split(':',1)
|
|
||||||
msg.add_header(name,(val % v.__dict__).strip())
|
|
||||||
msg.set_payload(body % v.__dict__)
|
|
||||||
# add headers if missing from old template
|
|
||||||
if 'to' not in msg:
|
|
||||||
msg.add_header('To',v.sender)
|
|
||||||
if 'from' not in msg:
|
|
||||||
msg.add_header('From','postmaster@%s'%v.receiver)
|
|
||||||
if 'auto-submitted' not in msg:
|
|
||||||
msg.add_header('Auto-Submitted','auto-generated')
|
|
||||||
return msg
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import spf
|
|
||||||
q = spf.query('192.168.9.50',
|
|
||||||
'SRS0=pmeHL=RH==stuart@example.com',
|
|
||||||
'red.example.com',receiver='mail.example.com')
|
|
||||||
q.result = 'softfail'
|
|
||||||
q.perm_error = None
|
|
||||||
msg = create_msg(q,['charlie@example.com'],None,
|
|
||||||
"""From: postmaster@%(receiver)s
|
|
||||||
To: %(sender)s
|
|
||||||
Subject: Test
|
|
||||||
|
|
||||||
Test DSN template
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
print(msg.as_string())
|
|
||||||
# print(send_dsn(f,msg.as_string()))
|
|
||||||
# print(send_dsn(q.s,'mail.example.com',msg.as_string()))
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2005 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
|
|
||||||
# Heuristically determine whether a domain name is for a dynamic IP.
|
|
||||||
|
|
||||||
# examples we don't yet recognize:
|
|
||||||
#
|
|
||||||
# wiley-268-8196.roadrunner.nf.net at ('205.251.174.46', 4810)
|
|
||||||
# cbl-sd-02-79.aster.com.do at ('200.88.62.79', 4153)
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
import re
|
|
||||||
|
|
||||||
ip3 = re.compile('[0-9]{1,3}')
|
|
||||||
hpats = (
|
|
||||||
'h[0-9a-f]{12}[.]',
|
|
||||||
'h\d*n\d*c\d*o\d*\.',
|
|
||||||
'pcp\d{6,10}pcs[.]',
|
|
||||||
'no-reverse',
|
|
||||||
'S[0-9a-f]{16}[.][a-z]{2}[.]',
|
|
||||||
'user<3>\.',
|
|
||||||
'[Cc]ust<3>\.',
|
|
||||||
'^<3>\.',
|
|
||||||
'ppp[^.]*<3>\.',
|
|
||||||
'-ppp\d*\.',
|
|
||||||
'\d*-<3>\.',
|
|
||||||
'[0-9a-f]{1,3}-<3>\.',
|
|
||||||
'p<3>\.pool',
|
|
||||||
'h<3>\.',
|
|
||||||
'xdsl-\d*\.',
|
|
||||||
'-\d*-\d*\.',
|
|
||||||
'\.adsl\.',
|
|
||||||
'\.cable\.'
|
|
||||||
)
|
|
||||||
rehmac = re.compile('|'.join(hpats))
|
|
||||||
|
|
||||||
def is_dynip(host,addr):
|
|
||||||
"""Return True if hostname is for a dynamic ip.
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
>>> is_dynip('post3.fabulousdealz.com','69.60.99.112')
|
|
||||||
False
|
|
||||||
>>> is_dynip('adsl-69-208-201-177.dsl.emhril.ameritech.net','69.208.201.177')
|
|
||||||
True
|
|
||||||
>>> is_dynip('[1.2.3.4]','1.2.3.4')
|
|
||||||
True
|
|
||||||
>>> is_dynip('c-71-63-151-151.hsd1.mn.comcast.net','71.63.151.151')
|
|
||||||
True
|
|
||||||
"""
|
|
||||||
if host.startswith('[') and host.endswith(']'):
|
|
||||||
return True # no ptr
|
|
||||||
if addr:
|
|
||||||
if host.find(addr) >= 0: return True
|
|
||||||
if addr.find(':') >= 0: return False # IP6
|
|
||||||
a = addr.split('.')
|
|
||||||
ia = list(map(int,a))
|
|
||||||
h = host
|
|
||||||
m = ip3.findall(host)
|
|
||||||
if m:
|
|
||||||
g = list(map(int,m))[:4]
|
|
||||||
ia3 = (ia[1:],ia[:3])
|
|
||||||
if g[-3:] in ia3: return True
|
|
||||||
if g[0] == ia[3] and g[1:3] == ia[:2]: return True
|
|
||||||
if g[-2:] == ia[2:]: return True
|
|
||||||
g.reverse()
|
|
||||||
if g[:3] in ia3: return True
|
|
||||||
if g[:2] == ia[2:]: return True
|
|
||||||
if ia[2:] in (g[:2],g[-2:]): return True
|
|
||||||
for m in ip3.finditer(host):
|
|
||||||
if int(m.group()) == ia[3]:
|
|
||||||
h = host[:m.start()] + '<3>' + host[m.end():]
|
|
||||||
break
|
|
||||||
if rehmac.search(h): return True
|
|
||||||
if host.find(''.join(a[:3])) >= 0: return True
|
|
||||||
if host.find(''.join(a[1:])) >= 0: return True
|
|
||||||
x = "%02x%02x%02x%02x" % tuple(ia)
|
|
||||||
if host.lower().find(x) >= 0: return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import fileinput
|
|
||||||
import sets
|
|
||||||
seen = sets.Set()
|
|
||||||
for ln in fileinput.input():
|
|
||||||
a = ln.split()
|
|
||||||
if a[3:5] == ['connect','from']:
|
|
||||||
host = a[5]
|
|
||||||
if host.startswith('[') and host.endswith(']'):
|
|
||||||
continue # no PTR
|
|
||||||
ip = a[7][2:-2]
|
|
||||||
if ip in seen: continue
|
|
||||||
seen.add(ip)
|
|
||||||
if is_dynip(host,ip):
|
|
||||||
print('%s\t%s DYN' % (ip,host))
|
|
||||||
else:
|
|
||||||
print('%s\t%s' % (ip,host))
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
import time
|
|
||||||
import shelve
|
|
||||||
import thread
|
|
||||||
import logging
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
log = logging.getLogger('milter.greylist')
|
|
||||||
|
|
||||||
def quoteAddress(s):
|
|
||||||
'''Quote an address so that it's safe to store in the file-system.
|
|
||||||
Address can either be a domain name, or local part.
|
|
||||||
Returns the quoted address.'''
|
|
||||||
|
|
||||||
s = urllib.quote(s, '@_-+~!.%')
|
|
||||||
if s.startswith('.'): s = '%2e' + s[1:]
|
|
||||||
return s
|
|
||||||
|
|
||||||
class Record(object):
|
|
||||||
__slots__ = ( 'firstseen', 'lastseen', 'umis', 'cnt' )
|
|
||||||
|
|
||||||
def __init__(self,timeinc=0):
|
|
||||||
now = time.time() + timeinc
|
|
||||||
self.firstseen = now
|
|
||||||
self.lastseen = now
|
|
||||||
self.cnt = 0
|
|
||||||
self.umis = None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Grey[%s:%s:%s:%d]" % (
|
|
||||||
time.ctime(self.firstseen),time.ctime(self.lastseen),
|
|
||||||
self.umis,self.cnt
|
|
||||||
)
|
|
||||||
|
|
||||||
class Greylist(object):
|
|
||||||
|
|
||||||
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
|
||||||
self.ignoreLastByte = False
|
|
||||||
self.greylist_time = grey_time * 60 # minutes
|
|
||||||
self.greylist_expire = grey_expire * 3600 # hours
|
|
||||||
self.greylist_retain = grey_retain * 24 * 3600 # days
|
|
||||||
self.dbp = shelve.open(dbname,'c',protocol=2)
|
|
||||||
self.lock = thread.allocate_lock()
|
|
||||||
|
|
||||||
def export_csv(self,fp,timeinc=0):
|
|
||||||
"Export records to csv."
|
|
||||||
import csv
|
|
||||||
dbp = self.dbp
|
|
||||||
w = csv.writer(fp)
|
|
||||||
now = time.time() + timeinc
|
|
||||||
for key, r in dbp.iteritems():
|
|
||||||
if now > r.lastseen + self.greylist_retain: continue
|
|
||||||
ip,sender,recipient = key.rsplit(':',2)
|
|
||||||
w.writerow([ip,sender,recipient,r.firstseen,r.lastseen,r.cnt,r.umis])
|
|
||||||
|
|
||||||
def clean(self,timeinc=0):
|
|
||||||
"Delete records past the retention limit."
|
|
||||||
now = time.time() + timeinc
|
|
||||||
cnt = 0
|
|
||||||
dbp = self.dbp
|
|
||||||
for key, r in dbp.iteritems():
|
|
||||||
#print(key,r,time.ctime(now))
|
|
||||||
if now > r.lastseen + self.greylist_retain:
|
|
||||||
self.lock.acquire()
|
|
||||||
try:
|
|
||||||
r = dbp[key]
|
|
||||||
now = time.time() + timeinc
|
|
||||||
if now > r.lastseen + self.greylist_retain:
|
|
||||||
del dbp[key]
|
|
||||||
cnt += 1
|
|
||||||
finally:
|
|
||||||
self.lock.release()
|
|
||||||
return cnt
|
|
||||||
|
|
||||||
def check(self,ip,sender,recipient,timeinc=0):
|
|
||||||
"Return number of allowed messages for greylist triple."
|
|
||||||
sender = quoteAddress(sender)
|
|
||||||
recipient = quoteAddress(recipient)
|
|
||||||
key = ip + ':' + sender + ':' + recipient
|
|
||||||
self.lock.acquire()
|
|
||||||
try:
|
|
||||||
dbp = self.dbp
|
|
||||||
try:
|
|
||||||
r = dbp[key]
|
|
||||||
now = time.time() + timeinc
|
|
||||||
if now > r.lastseen + self.greylist_retain:
|
|
||||||
# expired
|
|
||||||
log.debug('Expired greylist: %s',key)
|
|
||||||
r = Record(timeinc)
|
|
||||||
elif now < r.firstseen + self.greylist_time + 5:
|
|
||||||
# still greylisted
|
|
||||||
log.debug('Early greylist: %s',key)
|
|
||||||
#r = Record(timeinc)
|
|
||||||
r.lastseen = now
|
|
||||||
elif r.cnt or now < r.firstseen + self.greylist_expire:
|
|
||||||
# in greylist window or active
|
|
||||||
r.lastseen = now
|
|
||||||
r.cnt += 1
|
|
||||||
log.debug('Active greylist(%d): %s',r.cnt,key)
|
|
||||||
else:
|
|
||||||
# passed greylist window
|
|
||||||
log.debug('Late greylist: %s',key)
|
|
||||||
r = Record(timeinc)
|
|
||||||
dbp[key] = r
|
|
||||||
except:
|
|
||||||
r = Record(timeinc)
|
|
||||||
dbp[key] = r
|
|
||||||
dbp.sync()
|
|
||||||
finally:
|
|
||||||
self.lock.release()
|
|
||||||
return r.cnt
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.dbp.close()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
g = Greylist(sys.argv[1],5,24,36)
|
|
||||||
try:
|
|
||||||
g.export_csv(sys.stdout)
|
|
||||||
finally: g.close()
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import time
|
|
||||||
import logging
|
|
||||||
import urllib
|
|
||||||
import sqlite3
|
|
||||||
try:
|
|
||||||
import thread
|
|
||||||
except:
|
|
||||||
import _thread as thread
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
log = logging.getLogger('milter.greylist')
|
|
||||||
|
|
||||||
_db_lock = thread.allocate_lock()
|
|
||||||
|
|
||||||
class Greylist(object):
|
|
||||||
|
|
||||||
def __init__(self,dbname,grey_time=10,grey_expire=4,grey_retain=36):
|
|
||||||
self.ignoreLastByte = False
|
|
||||||
self.greylist_time = grey_time * 60 # minutes
|
|
||||||
self.greylist_expire = grey_expire * 3600 # hours
|
|
||||||
self.greylist_retain = grey_retain * 24 * 3600 # days
|
|
||||||
self.conn = sqlite3.connect(dbname)
|
|
||||||
self.conn.row_factory = sqlite3.Row
|
|
||||||
try:
|
|
||||||
self.conn.execute('''create table greylist(
|
|
||||||
ip text , sender text, recipient text,
|
|
||||||
firstseen timestamp, lastseen timestamp, cnt integer, umis text,
|
|
||||||
primary key (ip,sender,recipient))''')
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
def import_csv(self,fp):
|
|
||||||
import csv
|
|
||||||
rdr = csv.reader(fp)
|
|
||||||
cur = self.conn.execute('begin immediate')
|
|
||||||
try:
|
|
||||||
for r in rdr:
|
|
||||||
cur.execute('''insert into
|
|
||||||
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
|
|
||||||
values(?,?,?,?,?,?,?)''', r)
|
|
||||||
self.conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close();
|
|
||||||
|
|
||||||
def clean(self,timeinc=0):
|
|
||||||
"Delete records past the retention limit."
|
|
||||||
now = time.time() + timeinc - self.greylist_retain
|
|
||||||
cur = self.conn.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute('delete from greylist where lastseen < ?',(now,))
|
|
||||||
cnt = cur.rowcount
|
|
||||||
self.conn.commit()
|
|
||||||
finally: cur.close()
|
|
||||||
return cnt
|
|
||||||
|
|
||||||
def check(self,ip,sender,recipient,timeinc=0):
|
|
||||||
"Return number of allowed messages for greylist triple."
|
|
||||||
_db_lock.acquire()
|
|
||||||
cur = self.conn.execute('begin immediate')
|
|
||||||
try:
|
|
||||||
cur.execute('''select firstseen,lastseen,cnt,umis from greylist where
|
|
||||||
ip=? and sender=? and recipient=?''',(ip,sender,recipient))
|
|
||||||
r = cur.fetchone()
|
|
||||||
now = time.time() + timeinc
|
|
||||||
cnt = 0
|
|
||||||
if not r:
|
|
||||||
cur.execute('''insert into
|
|
||||||
greylist(ip,sender,recipient,firstseen,lastseen,cnt,umis)
|
|
||||||
values(?,?,?,?,?,?,?)''', (ip,sender,recipient,now,now,0,None))
|
|
||||||
elif now > r['lastseen'] + self.greylist_retain:
|
|
||||||
# expired
|
|
||||||
log.debug('Expired greylist: %s:%s:%s',ip,sender,recipient)
|
|
||||||
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
|
||||||
where ip=? and sender=? and recipient=?''',
|
|
||||||
(now,now,0,None,ip,sender,recipient))
|
|
||||||
elif now < r['firstseen'] + self.greylist_time + 5:
|
|
||||||
# still greylisted
|
|
||||||
log.debug('Early greylist: %s:%s:%s',ip,sender,recipient)
|
|
||||||
#r = Record()
|
|
||||||
cur.execute('''update greylist set lastseen=?
|
|
||||||
where ip=? and sender=? and recipient=?''',
|
|
||||||
(now,ip,sender,recipient))
|
|
||||||
elif r['cnt'] or now < r['firstseen'] + self.greylist_expire:
|
|
||||||
# in greylist window or active
|
|
||||||
cnt = r['cnt'] + 1
|
|
||||||
cur.execute('''update greylist set lastseen=?,cnt=?
|
|
||||||
where ip=? and sender=? and recipient=?''',
|
|
||||||
(now,cnt,ip,sender,recipient))
|
|
||||||
log.debug('Active greylist(%d): %s:%s:%s',cnt,ip,sender,recipient)
|
|
||||||
else:
|
|
||||||
# passed greylist window
|
|
||||||
log.debug('Late greylist: %s:%s:%s',ip,sender,recipient)
|
|
||||||
cur.execute('''update greylist set firstseen=?,lastseen=?,cnt=?,umis=?
|
|
||||||
where ip=? and sender=? and recipient=?''',
|
|
||||||
(now,now,0,None,ip,sender,recipient))
|
|
||||||
self.conn.commit()
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
_db_lock.release()
|
|
||||||
return cnt
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.conn.close()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
g = Greylist(sys.argv[1])
|
|
||||||
try:
|
|
||||||
g.import_csv(sys.stdin)
|
|
||||||
finally: g.close()
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2001 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
|
|
||||||
import os
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
class PLock(object):
|
|
||||||
"A simple /etc/passwd style lock,update,rename protocol for updating files."
|
|
||||||
def __init__(self,basename):
|
|
||||||
self.basename = basename
|
|
||||||
self.fp = None
|
|
||||||
|
|
||||||
def lock(self,lockname=None,mode=0o660,strict_perms=False):
|
|
||||||
"Start an update transaction. Return FILE to write new version."
|
|
||||||
self.unlock()
|
|
||||||
if not lockname:
|
|
||||||
lockname = self.basename + '.lock'
|
|
||||||
self.lockname = lockname
|
|
||||||
try:
|
|
||||||
st = os.stat(self.basename)
|
|
||||||
mode |= st.st_mode
|
|
||||||
except OSError: pass
|
|
||||||
u = os.umask(0o2)
|
|
||||||
try:
|
|
||||||
fd = os.open(lockname,os.O_WRONLY+os.O_CREAT+os.O_EXCL,mode)
|
|
||||||
finally:
|
|
||||||
os.umask(u)
|
|
||||||
self.fp = os.fdopen(fd,'w')
|
|
||||||
try:
|
|
||||||
os.chown(self.lockname,-1,st.st_gid)
|
|
||||||
except:
|
|
||||||
if strict_perms:
|
|
||||||
self.unlock()
|
|
||||||
raise
|
|
||||||
return self.fp
|
|
||||||
|
|
||||||
def wlock(self,lockname=None):
|
|
||||||
"Wait until lock is free, then start an update transaction."
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
return self.lock(lockname)
|
|
||||||
except OSError:
|
|
||||||
sleep(2)
|
|
||||||
|
|
||||||
def commit(self,backname=None):
|
|
||||||
"Commit update transaction with optional backup file."
|
|
||||||
if not self.fp:
|
|
||||||
raise IOError("File not locked")
|
|
||||||
self.fp.close()
|
|
||||||
self.fp = None
|
|
||||||
if backname:
|
|
||||||
try:
|
|
||||||
os.remove(backname)
|
|
||||||
except OSError: pass
|
|
||||||
os.link(self.basename,backname)
|
|
||||||
os.rename(self.lockname,self.basename)
|
|
||||||
|
|
||||||
def unlock(self):
|
|
||||||
"Cancel update transaction."
|
|
||||||
if self.fp:
|
|
||||||
try:
|
|
||||||
self.fp.close()
|
|
||||||
except: pass
|
|
||||||
self.fp = None
|
|
||||||
os.remove(self.lockname)
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
try:
|
|
||||||
try:
|
|
||||||
from berkeleydb import db
|
|
||||||
except:
|
|
||||||
from bsddb3 import db
|
|
||||||
class DB(object):
|
|
||||||
def open(self,fname,mode):
|
|
||||||
if mode == 'r': flags = db.DB_RDONLY
|
|
||||||
else: raise RuntimeException('unsupported mode')
|
|
||||||
self.f = db.DB()
|
|
||||||
self.f.open(fname,flags=flags)
|
|
||||||
def __contains__(self,key):
|
|
||||||
return not not self.f.get(key)
|
|
||||||
def __getitem__(self,key):
|
|
||||||
v = self.f.get(key)
|
|
||||||
if not v: raise KeyError(key)
|
|
||||||
return v
|
|
||||||
def close(self):
|
|
||||||
self.f.close()
|
|
||||||
def dbmopen(fname,mode):
|
|
||||||
f = DB()
|
|
||||||
f.open(fname,mode)
|
|
||||||
return f
|
|
||||||
except ModuleNotFoundError: raise
|
|
||||||
except:
|
|
||||||
import anydbm as dbm
|
|
||||||
dbmopen = dbm.open
|
|
||||||
|
|
||||||
class MTAPolicy(object):
|
|
||||||
"Get SPF policy by result from sendmail style access file."
|
|
||||||
def __init__(self,sender,conf,access_file=None):
|
|
||||||
if not access_file:
|
|
||||||
access_file = conf.access_file
|
|
||||||
self.use_nulls = conf.access_file_nulls
|
|
||||||
try:
|
|
||||||
self.use_colon = conf.access_file_colon
|
|
||||||
except:
|
|
||||||
self.use_colon = True
|
|
||||||
self.sender = sender
|
|
||||||
self.domain = sender.split('@')[-1].lower()
|
|
||||||
self.acf = None
|
|
||||||
self.access_file = access_file
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.acf:
|
|
||||||
self.acf.close()
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.acf = None
|
|
||||||
if self.access_file:
|
|
||||||
try:
|
|
||||||
self.acf = dbmopen(self.access_file,'r')
|
|
||||||
except:
|
|
||||||
print('%s: Cannot open for reading'%self.access_file)
|
|
||||||
raise
|
|
||||||
return self
|
|
||||||
def __exit__(self,t,v,b): self.close()
|
|
||||||
|
|
||||||
def getPolicy(self,pfx):
|
|
||||||
acf = self.acf
|
|
||||||
if not acf: return None
|
|
||||||
if self.use_nulls: sfx = b'\x00'
|
|
||||||
else: sfx = b''
|
|
||||||
if self.use_colon:
|
|
||||||
sep = b':'
|
|
||||||
else:
|
|
||||||
sep = b'!'
|
|
||||||
pfx = pfx.encode() + sep
|
|
||||||
try: # try with localpart@domain
|
|
||||||
return acf[pfx + self.sender.encode() + sfx].rstrip(b'\x00').decode()
|
|
||||||
except KeyError:
|
|
||||||
try: # try with domain
|
|
||||||
d = self.domain.encode()
|
|
||||||
k = pfx + d + sfx
|
|
||||||
while not k in acf and b'.' in d:
|
|
||||||
# check partial domains
|
|
||||||
d = b'.'.join(d.split(b'.')[1:])
|
|
||||||
k = pfx + b'.' + d + sfx
|
|
||||||
return acf[k].rstrip(b'\x00').decode()
|
|
||||||
except KeyError:
|
|
||||||
try: # try bare prefix
|
|
||||||
return acf[pfx + sfx].rstrip(b'\x00').decode()
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
return acf[pfx[:-1] + sfx].rstrip(b'\x00').decode()
|
|
||||||
except KeyError:
|
|
||||||
return None
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
"""Pure Python IP6 parsing and formatting
|
|
||||||
|
|
||||||
Copyright (c) 2006 Stuart Gathman <stuart@bmsi.com>
|
|
||||||
|
|
||||||
This module is free software, and you may redistribute it and/or modify
|
|
||||||
it under the same terms as Python itself, so long as this copyright message
|
|
||||||
and disclaimer are retained in their original form.
|
|
||||||
"""
|
|
||||||
from __future__ import print_function
|
|
||||||
import struct
|
|
||||||
#from spf import RE_IP4
|
|
||||||
import re
|
|
||||||
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
|
||||||
RE_IP4 = re.compile(PAT_IP4+'$')
|
|
||||||
|
|
||||||
def inet_ntop(s):
|
|
||||||
"""
|
|
||||||
Convert ip6 address to standard hex notation.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0xFFFF,0x0102,0x0304))
|
|
||||||
'::FFFF:1.2.3.4'
|
|
||||||
|
|
||||||
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0,0,0,0x0102,0x0304))
|
|
||||||
'1234:5678::102:304'
|
|
||||||
|
|
||||||
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0x1234,0x5678,0,0x0102,0x0304))
|
|
||||||
'::1234:5678:0:102:304'
|
|
||||||
|
|
||||||
>>> inet_ntop(struct.pack("!HHHHHHHH",0x1234,0x5678,0,0x0102,0x0304,0,0,0))
|
|
||||||
'1234:5678:0:102:304::'
|
|
||||||
|
|
||||||
>>> inet_ntop(struct.pack("!HHHHHHHH",0,0,0,0,0,0,0,0))
|
|
||||||
'::'
|
|
||||||
"""
|
|
||||||
# convert to 8 words
|
|
||||||
a = struct.unpack("!HHHHHHHH",s)
|
|
||||||
n = (0,0,0,0,0,0,0,0) # null ip6
|
|
||||||
if a == n: return '::'
|
|
||||||
# check for ip4 mapped
|
|
||||||
if a[:5] == (0,0,0,0,0) and a[5] in (0,0xFFFF):
|
|
||||||
ip4 = '.'.join([str(i) for i in struct.unpack("!BBBB",s[12:])])
|
|
||||||
if a[5]:
|
|
||||||
return "::FFFF:" + ip4
|
|
||||||
return "::" + ip4
|
|
||||||
# find index of longest sequence of 0
|
|
||||||
for l in (7,6,5,4,3,2,1):
|
|
||||||
e = n[:l]
|
|
||||||
for i in range(9-l):
|
|
||||||
if a[i:i+l] == e:
|
|
||||||
if i == 0:
|
|
||||||
return ':'+':%x'*(8-l) % a[l:]
|
|
||||||
if i == 8 - l:
|
|
||||||
return '%x:'*(8-l) % a[:-l] + ':'
|
|
||||||
return '%x:'*i % a[:i] + ':%x'*(8-l-i) % a[i+l:]
|
|
||||||
return "%x:%x:%x:%x:%x:%x:%x:%x" % a
|
|
||||||
|
|
||||||
def inet_pton(p):
|
|
||||||
"""
|
|
||||||
Convert ip6 standard hex notation to ip6 address.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
>>> struct.unpack('!HHHHHHHH',inet_pton('::'))
|
|
||||||
(0, 0, 0, 0, 0, 0, 0, 0)
|
|
||||||
|
|
||||||
>>> struct.unpack('!HHHHHHHH',inet_pton('::1234'))
|
|
||||||
(0, 0, 0, 0, 0, 0, 0, 4660)
|
|
||||||
|
|
||||||
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::'))
|
|
||||||
(4660, 0, 0, 0, 0, 0, 0, 0)
|
|
||||||
|
|
||||||
>>> struct.unpack('!HHHHHHHH',inet_pton('1234::5678'))
|
|
||||||
(4660, 0, 0, 0, 0, 0, 0, 22136)
|
|
||||||
|
|
||||||
>>> struct.unpack('!HHHHHHHH',inet_pton('::FFFF:1.2.3.4'))
|
|
||||||
(0, 0, 0, 0, 0, 65535, 258, 772)
|
|
||||||
|
|
||||||
>>> struct.unpack('!HHHHHHHH',inet_pton('1.2.3.4'))
|
|
||||||
(0, 0, 0, 0, 0, 65535, 258, 772)
|
|
||||||
|
|
||||||
>>> try: inet_pton('::1.2.3.4.5')
|
|
||||||
... except ValueError as x: print(x)
|
|
||||||
::1.2.3.4.5
|
|
||||||
"""
|
|
||||||
if p == '::':
|
|
||||||
return b'\0'*16
|
|
||||||
s = p
|
|
||||||
m = RE_IP4.search(s)
|
|
||||||
try:
|
|
||||||
if m:
|
|
||||||
pos = m.start()
|
|
||||||
ip4 = [int(i) for i in s[pos:].split('.')]
|
|
||||||
if not pos:
|
|
||||||
return struct.pack('!QLBBBB',0,65535,*ip4)
|
|
||||||
s = s[:pos]+'%x%02x:%x%02x'%tuple(ip4)
|
|
||||||
a = s.split('::')
|
|
||||||
if len(a) == 2:
|
|
||||||
l,r = a
|
|
||||||
if not l:
|
|
||||||
r = r.split(':')
|
|
||||||
return struct.pack('!HHHHHHHH',
|
|
||||||
*[0]*(8-len(r)) + [int(s,16) for s in r])
|
|
||||||
if not r:
|
|
||||||
l = l.split(':')
|
|
||||||
return struct.pack('!HHHHHHHH',
|
|
||||||
*[int(s,16) for s in l] + [0]*(8-len(l)))
|
|
||||||
l = l.split(':')
|
|
||||||
r = r.split(':')
|
|
||||||
return struct.pack('!HHHHHHHH',
|
|
||||||
*[int(s,16) for s in l] + [0]*(8-len(l)-len(r))
|
|
||||||
+ [int(s,16) for s in r])
|
|
||||||
if len(a) == 1:
|
|
||||||
return struct.pack('!HHHHHHHH',
|
|
||||||
*[int(s,16) for s in a[0].split(':')])
|
|
||||||
except ValueError: pass
|
|
||||||
raise ValueError(p)
|
|
||||||
@@ -1,552 +0,0 @@
|
|||||||
"""A parser for SGML, using the derived class as a static DTD."""
|
|
||||||
|
|
||||||
# XXX This only supports those SGML features used by HTML.
|
|
||||||
|
|
||||||
# XXX There should be a way to distinguish between PCDATA (parsed
|
|
||||||
# character data -- the normal case), RCDATA (replaceable character
|
|
||||||
# data -- only char and entity references and end tags are special)
|
|
||||||
# and CDATA (character data -- only end tags are special). RCDATA is
|
|
||||||
# not supported at all.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
try:
|
|
||||||
import _markupbase
|
|
||||||
except:
|
|
||||||
import markupbase as _markupbase
|
|
||||||
import re
|
|
||||||
|
|
||||||
__all__ = ["SGMLParser", "SGMLParseError"]
|
|
||||||
|
|
||||||
# Regular expressions used for parsing
|
|
||||||
|
|
||||||
interesting = re.compile('[&<]')
|
|
||||||
incomplete = re.compile('&([a-zA-Z][a-zA-Z0-9]*|#[0-9]*)?|'
|
|
||||||
'<([a-zA-Z][^<>]*|'
|
|
||||||
'/([a-zA-Z][^<>]*)?|'
|
|
||||||
'![^<>]*)?')
|
|
||||||
|
|
||||||
entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]')
|
|
||||||
charref = re.compile('&#([0-9]+)[^0-9]')
|
|
||||||
|
|
||||||
starttagopen = re.compile('<[>a-zA-Z]')
|
|
||||||
shorttagopen = re.compile('<[a-zA-Z][-.a-zA-Z0-9]*/')
|
|
||||||
shorttag = re.compile('<([a-zA-Z][-.a-zA-Z0-9]*)/([^/]*)/')
|
|
||||||
piclose = re.compile('>')
|
|
||||||
endbracket = re.compile('[<>]')
|
|
||||||
tagfind = re.compile('[a-zA-Z][-_.a-zA-Z0-9]*')
|
|
||||||
attrfind = re.compile(
|
|
||||||
r'\s*([a-zA-Z_][-:.a-zA-Z_0-9]*)(\s*=\s*'
|
|
||||||
r'(\'[^\']*\'|"[^"]*"|[][\-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?')
|
|
||||||
|
|
||||||
|
|
||||||
class SGMLParseError(RuntimeError):
|
|
||||||
"""Exception raised for all parse errors."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# SGML parser base class -- find tags and call handler functions.
|
|
||||||
# Usage: p = SGMLParser(); p.feed(data); ...; p.close().
|
|
||||||
# The dtd is defined by deriving a class which defines methods
|
|
||||||
# with special names to handle tags: start_foo and end_foo to handle
|
|
||||||
# <foo> and </foo>, respectively, or do_foo to handle <foo> by itself.
|
|
||||||
# (Tags are converted to lower case for this purpose.) The data
|
|
||||||
# between tags is passed to the parser by calling self.handle_data()
|
|
||||||
# with some data as argument (the data may be split up in arbitrary
|
|
||||||
# chunks). Entity references are passed by calling
|
|
||||||
# self.handle_entityref() with the entity reference as argument.
|
|
||||||
|
|
||||||
class SGMLParser(_markupbase.ParserBase):
|
|
||||||
# Definition of entities -- derived classes may override
|
|
||||||
entity_or_charref = re.compile('&(?:'
|
|
||||||
'([a-zA-Z][-.a-zA-Z0-9]*)|#([0-9]+)'
|
|
||||||
')(;?)')
|
|
||||||
|
|
||||||
def __init__(self, verbose=0):
|
|
||||||
"""Initialize and reset this instance."""
|
|
||||||
self.verbose = verbose
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Reset this instance. Loses all unprocessed data."""
|
|
||||||
self.__starttag_text = None
|
|
||||||
self.rawdata = ''
|
|
||||||
self.stack = []
|
|
||||||
self.lasttag = '???'
|
|
||||||
self.nomoretags = 0
|
|
||||||
self.literal = 0
|
|
||||||
_markupbase.ParserBase.reset(self)
|
|
||||||
|
|
||||||
def setnomoretags(self):
|
|
||||||
"""Enter literal mode (CDATA) till EOF.
|
|
||||||
|
|
||||||
Intended for derived classes only.
|
|
||||||
"""
|
|
||||||
self.nomoretags = self.literal = 1
|
|
||||||
|
|
||||||
def setliteral(self, *args):
|
|
||||||
"""Enter literal mode (CDATA).
|
|
||||||
|
|
||||||
Intended for derived classes only.
|
|
||||||
"""
|
|
||||||
self.literal = 1
|
|
||||||
|
|
||||||
def feed(self, data):
|
|
||||||
"""Feed some data to the parser.
|
|
||||||
|
|
||||||
Call this as often as you want, with as little or as much text
|
|
||||||
as you want (may include '\n'). (This just saves the text,
|
|
||||||
all the processing is done by goahead().)
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.rawdata = self.rawdata + data
|
|
||||||
self.goahead(0)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Handle the remaining data."""
|
|
||||||
self.goahead(1)
|
|
||||||
|
|
||||||
def error(self, message):
|
|
||||||
raise SGMLParseError(message)
|
|
||||||
|
|
||||||
# Internal -- handle data as far as reasonable. May leave state
|
|
||||||
# and data to be processed by a subsequent call. If 'end' is
|
|
||||||
# true, force handling all data as if followed by EOF marker.
|
|
||||||
def goahead(self, end):
|
|
||||||
rawdata = self.rawdata
|
|
||||||
i = 0
|
|
||||||
n = len(rawdata)
|
|
||||||
while i < n:
|
|
||||||
if self.nomoretags:
|
|
||||||
self.handle_data(rawdata[i:n])
|
|
||||||
i = n
|
|
||||||
break
|
|
||||||
match = interesting.search(rawdata, i)
|
|
||||||
if match: j = match.start()
|
|
||||||
else: j = n
|
|
||||||
if i < j:
|
|
||||||
self.handle_data(rawdata[i:j])
|
|
||||||
i = j
|
|
||||||
if i == n: break
|
|
||||||
if rawdata[i] == '<':
|
|
||||||
if starttagopen.match(rawdata, i):
|
|
||||||
if self.literal:
|
|
||||||
self.handle_data(rawdata[i])
|
|
||||||
i = i+1
|
|
||||||
continue
|
|
||||||
k = self.parse_starttag(i)
|
|
||||||
if k < 0: break
|
|
||||||
i = k
|
|
||||||
continue
|
|
||||||
if rawdata.startswith("</", i):
|
|
||||||
k = self.parse_endtag(i)
|
|
||||||
if k < 0: break
|
|
||||||
i = k
|
|
||||||
self.literal = 0
|
|
||||||
continue
|
|
||||||
if self.literal:
|
|
||||||
if n > (i + 1):
|
|
||||||
self.handle_data("<")
|
|
||||||
i = i+1
|
|
||||||
else:
|
|
||||||
# incomplete
|
|
||||||
break
|
|
||||||
continue
|
|
||||||
if rawdata.startswith("<!--", i):
|
|
||||||
# Strictly speaking, a comment is --.*--
|
|
||||||
# within a declaration tag <!...>.
|
|
||||||
# This should be removed,
|
|
||||||
# and comments handled only in parse_declaration.
|
|
||||||
k = self.parse_comment(i)
|
|
||||||
if k < 0: break
|
|
||||||
i = k
|
|
||||||
continue
|
|
||||||
if rawdata.startswith("<?", i):
|
|
||||||
k = self.parse_pi(i)
|
|
||||||
if k < 0: break
|
|
||||||
i = i+k
|
|
||||||
continue
|
|
||||||
if rawdata.startswith("<!", i):
|
|
||||||
# This is some sort of declaration; in "HTML as
|
|
||||||
# deployed," this should only be the document type
|
|
||||||
# declaration ("<!DOCTYPE html...>").
|
|
||||||
k = self.parse_declaration(i)
|
|
||||||
if k < 0: break
|
|
||||||
i = k
|
|
||||||
continue
|
|
||||||
elif rawdata[i] == '&':
|
|
||||||
if self.literal:
|
|
||||||
self.handle_data(rawdata[i])
|
|
||||||
i = i+1
|
|
||||||
continue
|
|
||||||
match = charref.match(rawdata, i)
|
|
||||||
if match:
|
|
||||||
name = match.group(1)
|
|
||||||
self.handle_charref(name)
|
|
||||||
i = match.end(0)
|
|
||||||
if rawdata[i-1] != ';': i = i-1
|
|
||||||
continue
|
|
||||||
match = entityref.match(rawdata, i)
|
|
||||||
if match:
|
|
||||||
name = match.group(1)
|
|
||||||
self.handle_entityref(name)
|
|
||||||
i = match.end(0)
|
|
||||||
if rawdata[i-1] != ';': i = i-1
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
self.error('neither < nor & ??')
|
|
||||||
# We get here only if incomplete matches but
|
|
||||||
# nothing else
|
|
||||||
match = incomplete.match(rawdata, i)
|
|
||||||
if not match:
|
|
||||||
self.handle_data(rawdata[i])
|
|
||||||
i = i+1
|
|
||||||
continue
|
|
||||||
j = match.end(0)
|
|
||||||
if j == n:
|
|
||||||
break # Really incomplete
|
|
||||||
self.handle_data(rawdata[i:j])
|
|
||||||
i = j
|
|
||||||
# end while
|
|
||||||
if end and i < n:
|
|
||||||
self.handle_data(rawdata[i:n])
|
|
||||||
i = n
|
|
||||||
self.rawdata = rawdata[i:]
|
|
||||||
# XXX if end: check for empty stack
|
|
||||||
|
|
||||||
# Extensions for the DOCTYPE scanner:
|
|
||||||
_decl_otherchars = '='
|
|
||||||
|
|
||||||
# Internal -- parse processing instr, return length or -1 if not terminated
|
|
||||||
def parse_pi(self, i):
|
|
||||||
rawdata = self.rawdata
|
|
||||||
if rawdata[i:i+2] != '<?':
|
|
||||||
self.error('unexpected call to parse_pi()')
|
|
||||||
match = piclose.search(rawdata, i+2)
|
|
||||||
if not match:
|
|
||||||
return -1
|
|
||||||
j = match.start(0)
|
|
||||||
self.handle_pi(rawdata[i+2: j])
|
|
||||||
j = match.end(0)
|
|
||||||
return j-i
|
|
||||||
|
|
||||||
def get_starttag_text(self):
|
|
||||||
return self.__starttag_text
|
|
||||||
|
|
||||||
# Internal -- handle starttag, return length or -1 if not terminated
|
|
||||||
def parse_starttag(self, i):
|
|
||||||
self.__starttag_text = None
|
|
||||||
start_pos = i
|
|
||||||
rawdata = self.rawdata
|
|
||||||
if shorttagopen.match(rawdata, i):
|
|
||||||
# SGML shorthand: <tag/data/ == <tag>data</tag>
|
|
||||||
# XXX Can data contain &... (entity or char refs)?
|
|
||||||
# XXX Can data contain < or > (tag characters)?
|
|
||||||
# XXX Can there be whitespace before the first /?
|
|
||||||
match = shorttag.match(rawdata, i)
|
|
||||||
if not match:
|
|
||||||
return -1
|
|
||||||
tag, data = match.group(1, 2)
|
|
||||||
self.__starttag_text = '<%s/' % tag
|
|
||||||
tag = tag.lower()
|
|
||||||
k = match.end(0)
|
|
||||||
self.finish_shorttag(tag, data)
|
|
||||||
self.__starttag_text = rawdata[start_pos:match.end(1) + 1]
|
|
||||||
return k
|
|
||||||
# XXX The following should skip matching quotes (' or ")
|
|
||||||
# As a shortcut way to exit, this isn't so bad, but shouldn't
|
|
||||||
# be used to locate the actual end of the start tag since the
|
|
||||||
# < or > characters may be embedded in an attribute value.
|
|
||||||
match = endbracket.search(rawdata, i+1)
|
|
||||||
if not match:
|
|
||||||
return -1
|
|
||||||
j = match.start(0)
|
|
||||||
# Now parse the data between i+1 and j into a tag and attrs
|
|
||||||
attrs = []
|
|
||||||
if rawdata[i:i+2] == '<>':
|
|
||||||
# SGML shorthand: <> == <last open tag seen>
|
|
||||||
k = j
|
|
||||||
tag = self.lasttag
|
|
||||||
else:
|
|
||||||
match = tagfind.match(rawdata, i+1)
|
|
||||||
if not match:
|
|
||||||
self.error('unexpected call to parse_starttag')
|
|
||||||
k = match.end(0)
|
|
||||||
tag = rawdata[i+1:k].lower()
|
|
||||||
self.lasttag = tag
|
|
||||||
while k < j:
|
|
||||||
match = attrfind.match(rawdata, k)
|
|
||||||
if not match: break
|
|
||||||
attrname, rest, attrvalue = match.group(1, 2, 3)
|
|
||||||
if not rest:
|
|
||||||
attrvalue = attrname
|
|
||||||
else:
|
|
||||||
if (attrvalue[:1] == "'" == attrvalue[-1:] or
|
|
||||||
attrvalue[:1] == '"' == attrvalue[-1:]):
|
|
||||||
# strip quotes
|
|
||||||
attrvalue = attrvalue[1:-1]
|
|
||||||
attrvalue = self.entity_or_charref.sub(
|
|
||||||
self._convert_ref, attrvalue)
|
|
||||||
attrs.append((attrname.lower(), attrvalue))
|
|
||||||
k = match.end(0)
|
|
||||||
if rawdata[j] == '>':
|
|
||||||
j = j+1
|
|
||||||
self.__starttag_text = rawdata[start_pos:j]
|
|
||||||
self.finish_starttag(tag, attrs)
|
|
||||||
return j
|
|
||||||
|
|
||||||
# Internal -- convert entity or character reference
|
|
||||||
def _convert_ref(self, match):
|
|
||||||
if match.group(2):
|
|
||||||
return self.convert_charref(match.group(2)) or \
|
|
||||||
'&#%s%s' % match.groups()[1:]
|
|
||||||
elif match.group(3):
|
|
||||||
return self.convert_entityref(match.group(1)) or \
|
|
||||||
'&%s;' % match.group(1)
|
|
||||||
else:
|
|
||||||
return '&%s' % match.group(1)
|
|
||||||
|
|
||||||
# Internal -- parse endtag
|
|
||||||
def parse_endtag(self, i):
|
|
||||||
rawdata = self.rawdata
|
|
||||||
match = endbracket.search(rawdata, i+1)
|
|
||||||
if not match:
|
|
||||||
return -1
|
|
||||||
j = match.start(0)
|
|
||||||
tag = rawdata[i+2:j].strip().lower()
|
|
||||||
if rawdata[j] == '>':
|
|
||||||
j = j+1
|
|
||||||
self.finish_endtag(tag)
|
|
||||||
return j
|
|
||||||
|
|
||||||
# Internal -- finish parsing of <tag/data/ (same as <tag>data</tag>)
|
|
||||||
def finish_shorttag(self, tag, data):
|
|
||||||
self.finish_starttag(tag, [])
|
|
||||||
self.handle_data(data)
|
|
||||||
self.finish_endtag(tag)
|
|
||||||
|
|
||||||
# Internal -- finish processing of start tag
|
|
||||||
# Return -1 for unknown tag, 0 for open-only tag, 1 for balanced tag
|
|
||||||
def finish_starttag(self, tag, attrs):
|
|
||||||
try:
|
|
||||||
method = getattr(self, 'start_' + tag)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
method = getattr(self, 'do_' + tag)
|
|
||||||
except AttributeError:
|
|
||||||
self.unknown_starttag(tag, attrs)
|
|
||||||
return -1
|
|
||||||
else:
|
|
||||||
self.handle_starttag(tag, method, attrs)
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
self.stack.append(tag)
|
|
||||||
self.handle_starttag(tag, method, attrs)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Internal -- finish processing of end tag
|
|
||||||
def finish_endtag(self, tag):
|
|
||||||
if not tag:
|
|
||||||
found = len(self.stack) - 1
|
|
||||||
if found < 0:
|
|
||||||
self.unknown_endtag(tag)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
if tag not in self.stack:
|
|
||||||
try:
|
|
||||||
method = getattr(self, 'end_' + tag)
|
|
||||||
except AttributeError:
|
|
||||||
self.unknown_endtag(tag)
|
|
||||||
else:
|
|
||||||
self.report_unbalanced(tag)
|
|
||||||
return
|
|
||||||
found = len(self.stack)
|
|
||||||
for i in range(found):
|
|
||||||
if self.stack[i] == tag: found = i
|
|
||||||
while len(self.stack) > found:
|
|
||||||
tag = self.stack[-1]
|
|
||||||
try:
|
|
||||||
method = getattr(self, 'end_' + tag)
|
|
||||||
except AttributeError:
|
|
||||||
method = None
|
|
||||||
if method:
|
|
||||||
self.handle_endtag(tag, method)
|
|
||||||
else:
|
|
||||||
self.unknown_endtag(tag)
|
|
||||||
del self.stack[-1]
|
|
||||||
|
|
||||||
# Overridable -- handle start tag
|
|
||||||
def handle_starttag(self, tag, method, attrs):
|
|
||||||
method(attrs)
|
|
||||||
|
|
||||||
# Overridable -- handle end tag
|
|
||||||
def handle_endtag(self, tag, method):
|
|
||||||
method()
|
|
||||||
|
|
||||||
# Example -- report an unbalanced </...> tag.
|
|
||||||
def report_unbalanced(self, tag):
|
|
||||||
if self.verbose:
|
|
||||||
print('*** Unbalanced </' + tag + '>')
|
|
||||||
print('*** Stack:', self.stack)
|
|
||||||
|
|
||||||
def convert_charref(self, name):
|
|
||||||
"""Convert character reference, may be overridden."""
|
|
||||||
try:
|
|
||||||
n = int(name)
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
if not 0 <= n <= 127:
|
|
||||||
return
|
|
||||||
return self.convert_codepoint(n)
|
|
||||||
|
|
||||||
def convert_codepoint(self, codepoint):
|
|
||||||
return chr(codepoint)
|
|
||||||
|
|
||||||
def handle_charref(self, name):
|
|
||||||
"""Handle character reference, no need to override."""
|
|
||||||
replacement = self.convert_charref(name)
|
|
||||||
if replacement is None:
|
|
||||||
self.unknown_charref(name)
|
|
||||||
else:
|
|
||||||
self.handle_data(replacement)
|
|
||||||
|
|
||||||
# Definition of entities -- derived classes may override
|
|
||||||
entitydefs = \
|
|
||||||
{'lt': '<', 'gt': '>', 'amp': '&', 'quot': '"', 'apos': '\''}
|
|
||||||
|
|
||||||
def convert_entityref(self, name):
|
|
||||||
"""Convert entity references.
|
|
||||||
|
|
||||||
As an alternative to overriding this method; one can tailor the
|
|
||||||
results by setting up the self.entitydefs mapping appropriately.
|
|
||||||
"""
|
|
||||||
table = self.entitydefs
|
|
||||||
if name in table:
|
|
||||||
return table[name]
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
def handle_entityref(self, name):
|
|
||||||
"""Handle entity references, no need to override."""
|
|
||||||
replacement = self.convert_entityref(name)
|
|
||||||
if replacement is None:
|
|
||||||
self.unknown_entityref(name)
|
|
||||||
else:
|
|
||||||
self.handle_data(replacement)
|
|
||||||
|
|
||||||
# Example -- handle data, should be overridden
|
|
||||||
def handle_data(self, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Example -- handle comment, could be overridden
|
|
||||||
def handle_comment(self, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Example -- handle declaration, could be overridden
|
|
||||||
def handle_decl(self, decl):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Example -- handle processing instruction, could be overridden
|
|
||||||
def handle_pi(self, data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# To be overridden -- handlers for unknown objects
|
|
||||||
def unknown_starttag(self, tag, attrs): pass
|
|
||||||
def unknown_endtag(self, tag): pass
|
|
||||||
def unknown_charref(self, ref): pass
|
|
||||||
def unknown_entityref(self, ref): pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestSGMLParser(SGMLParser):
|
|
||||||
|
|
||||||
def __init__(self, verbose=0):
|
|
||||||
self.testdata = ""
|
|
||||||
SGMLParser.__init__(self, verbose)
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
self.testdata = self.testdata + data
|
|
||||||
if len(repr(self.testdata)) >= 70:
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
data = self.testdata
|
|
||||||
if data:
|
|
||||||
self.testdata = ""
|
|
||||||
print('data:', repr(data))
|
|
||||||
|
|
||||||
def handle_comment(self, data):
|
|
||||||
self.flush()
|
|
||||||
r = repr(data)
|
|
||||||
if len(r) > 68:
|
|
||||||
r = r[:32] + '...' + r[-32:]
|
|
||||||
print('comment:', r)
|
|
||||||
|
|
||||||
def unknown_starttag(self, tag, attrs):
|
|
||||||
self.flush()
|
|
||||||
if not attrs:
|
|
||||||
print('start tag: <' + tag + '>')
|
|
||||||
else:
|
|
||||||
print('start tag: <' + tag, end=' ')
|
|
||||||
for name, value in attrs:
|
|
||||||
print(name + '=' + '"' + value + '"', end=' ')
|
|
||||||
print('>')
|
|
||||||
|
|
||||||
def unknown_endtag(self, tag):
|
|
||||||
self.flush()
|
|
||||||
print('end tag: </' + tag + '>')
|
|
||||||
|
|
||||||
def unknown_entityref(self, ref):
|
|
||||||
self.flush()
|
|
||||||
print('*** unknown entity ref: &' + ref + ';')
|
|
||||||
|
|
||||||
def unknown_charref(self, ref):
|
|
||||||
self.flush()
|
|
||||||
print('*** unknown char ref: &#' + ref + ';')
|
|
||||||
|
|
||||||
def unknown_decl(self, data):
|
|
||||||
self.flush()
|
|
||||||
print('*** unknown decl: [' + data + ']')
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
SGMLParser.close(self)
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
|
|
||||||
def test(args = None):
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if args is None:
|
|
||||||
args = sys.argv[1:]
|
|
||||||
|
|
||||||
if args and args[0] == '-s':
|
|
||||||
args = args[1:]
|
|
||||||
klass = SGMLParser
|
|
||||||
else:
|
|
||||||
klass = TestSGMLParser
|
|
||||||
|
|
||||||
if args:
|
|
||||||
file = args[0]
|
|
||||||
else:
|
|
||||||
file = 'test.html'
|
|
||||||
|
|
||||||
if file == '-':
|
|
||||||
f = sys.stdin
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
f = open(file, 'r')
|
|
||||||
except IOError as msg:
|
|
||||||
print(file, ":", msg)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
data = f.read()
|
|
||||||
if f is not sys.stdin:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
x = klass()
|
|
||||||
for c in data:
|
|
||||||
x.feed(c)
|
|
||||||
x.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
test()
|
|
||||||
-247
@@ -1,247 +0,0 @@
|
|||||||
## @package Milter.test
|
|
||||||
# A test framework for milters
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
import mime
|
|
||||||
try:
|
|
||||||
from io import BytesIO
|
|
||||||
except:
|
|
||||||
from StringIO import StringIO as BytesIO
|
|
||||||
import Milter
|
|
||||||
|
|
||||||
Milter.NOREPLY = Milter.CONTINUE
|
|
||||||
|
|
||||||
## Test mixin for unit testing %milter applications.
|
|
||||||
# This mixin overrides many Milter.MilterBase methods
|
|
||||||
# with stub versions that simply record what was done.
|
|
||||||
# @deprecated Use Milter.test.TestCtx
|
|
||||||
# @since 0.9.8
|
|
||||||
class TestBase(object):
|
|
||||||
|
|
||||||
def __init__(self,logfile='test/milter.log'):
|
|
||||||
self._protocol = 0
|
|
||||||
self.logfp = open(logfile,"a")
|
|
||||||
## The MAIL FROM for the current email being fed to the %milter
|
|
||||||
self._sender = None
|
|
||||||
## List of recipients deleted
|
|
||||||
self._delrcpt = []
|
|
||||||
## List of recipients added
|
|
||||||
self._addrcpt = []
|
|
||||||
## Macros defined
|
|
||||||
self._macros = { }
|
|
||||||
## The message body.
|
|
||||||
self._body = None
|
|
||||||
## True if the %milter replaced the message body.
|
|
||||||
self._bodyreplaced = False
|
|
||||||
## True if the %milter changed any headers.
|
|
||||||
self._headerschanged = False
|
|
||||||
## True if the %milter changed the envelope from.
|
|
||||||
self._envfromchanged = False
|
|
||||||
## Reply codes and messages set by the %milter
|
|
||||||
self._reply = None
|
|
||||||
## The rfc822 message object for the current email being fed to the %milter.
|
|
||||||
self._msg = None
|
|
||||||
## The protocol stage for macros returned
|
|
||||||
self._stage = None
|
|
||||||
## The macros returned by protocol stage
|
|
||||||
self._symlist = [ None, None, None, None, None, None, None ]
|
|
||||||
|
|
||||||
def _close(self):
|
|
||||||
if self.logfp:
|
|
||||||
self.logfp.close()
|
|
||||||
self.logfp = None
|
|
||||||
|
|
||||||
def log(self,*msg):
|
|
||||||
for i in msg: print(i,file=self.logfp,end=None)
|
|
||||||
print(file=self.logfp)
|
|
||||||
|
|
||||||
## Set a macro value.
|
|
||||||
# These are retrieved by the %milter with getsymval.
|
|
||||||
# @param name the macro name, as passed to getsymval
|
|
||||||
# @param val the macro value
|
|
||||||
def setsymval(self,name,val):
|
|
||||||
self._macros[name] = val
|
|
||||||
|
|
||||||
def getsymval(self,name):
|
|
||||||
stage = self._stage
|
|
||||||
if stage is not None and stage >= 0:
|
|
||||||
syms = self._symlist[stage]
|
|
||||||
if syms is not None and name not in syms:
|
|
||||||
return None
|
|
||||||
return self._macros.get(name,None)
|
|
||||||
|
|
||||||
def replacebody(self,chunk):
|
|
||||||
if self._body:
|
|
||||||
self._body.write(chunk)
|
|
||||||
self._bodyreplaced = True
|
|
||||||
else:
|
|
||||||
raise IOError("replacebody not called from eom()")
|
|
||||||
|
|
||||||
def chgfrom(self,sender,params=None):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("chgfrom not called from eom()")
|
|
||||||
self.log('chgfrom: sender=%s' % (sender))
|
|
||||||
self._envfromchanged = True
|
|
||||||
self._sender = sender
|
|
||||||
|
|
||||||
# TODO: write implement quarantine()
|
|
||||||
def quarantine(self,reason):
|
|
||||||
raise NotImplemented
|
|
||||||
|
|
||||||
# TODO: measure time between milter calls
|
|
||||||
def progress(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# FIXME: rfc822 indexing does not really reflect the way chg/add header
|
|
||||||
# work for a %milter
|
|
||||||
def chgheader(self,field,idx,value):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("chgheader not called from eom()")
|
|
||||||
self.log('chgheader: %s[%d]=%s' % (field,idx,value))
|
|
||||||
if value == '':
|
|
||||||
del self._msg[field]
|
|
||||||
else:
|
|
||||||
self._msg[field] = value
|
|
||||||
self._headerschanged = True
|
|
||||||
|
|
||||||
def addheader(self,field,value,idx=-1):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("addheader not called from eom()")
|
|
||||||
self.log('addheader: %s=%s' % (field,value))
|
|
||||||
self._msg[field] = value
|
|
||||||
self._headerschanged = True
|
|
||||||
|
|
||||||
def delrcpt(self,rcpt):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("delrcpt not called from eom()")
|
|
||||||
self._delrcpt.append(rcpt)
|
|
||||||
|
|
||||||
def addrcpt(self,rcpt):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("addrcpt not called from eom()")
|
|
||||||
self._addrcpt.append(rcpt)
|
|
||||||
|
|
||||||
## Save the reply codes and messages in self._reply.
|
|
||||||
def setreply(self,rcode,xcode,*msg):
|
|
||||||
self._reply = (rcode,xcode) + msg
|
|
||||||
|
|
||||||
def setsymlist(self,stage,macros):
|
|
||||||
if not self._actions & Milter.SETSYMLIST:
|
|
||||||
raise DisabledAction("SETSYMLIST")
|
|
||||||
if self._stage != -1:
|
|
||||||
raise RuntimeError("setsymlist may only be called from negotiate")
|
|
||||||
# not used yet, but just for grins we save the data
|
|
||||||
a = []
|
|
||||||
for m in macros:
|
|
||||||
try:
|
|
||||||
m = m.encode('utf8')
|
|
||||||
except: pass
|
|
||||||
try:
|
|
||||||
m = m.split(b' ')
|
|
||||||
except: pass
|
|
||||||
a += m
|
|
||||||
if len(a) > 5:
|
|
||||||
raise ValueError('setsymlist limited to 5 macros by MTA')
|
|
||||||
if self._symlist[stage] is not None:
|
|
||||||
raise ValueError('setsymlist already called for stage:'+stage)
|
|
||||||
print('setsymlist',stage,a)
|
|
||||||
self._symlist[stage] = set(a)
|
|
||||||
|
|
||||||
## Feed a file like object to the %milter. Calls envfrom, envrcpt for
|
|
||||||
# each recipient, header for each header field, body for each body
|
|
||||||
# block, and finally eom. A return code from the %milter other than
|
|
||||||
# CONTINUE returns immediately with that return code.
|
|
||||||
#
|
|
||||||
# This is a convenience method, a test could invoke the callbacks
|
|
||||||
# in sequence on its own - and for some complex tests, this may
|
|
||||||
# be necessary.
|
|
||||||
# @param fp the file with rfc2822 message stream
|
|
||||||
# @param sender the MAIL FROM
|
|
||||||
# @param rcpt RCPT TO - additional recipients may follow
|
|
||||||
def feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
|
|
||||||
self._body = None
|
|
||||||
self._bodyreplaced = False
|
|
||||||
self._headerschanged = False
|
|
||||||
self._reply = None
|
|
||||||
self._sender = '<%s>'%sender
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
# envfrom
|
|
||||||
self._stage = Milter.M_ENVFROM
|
|
||||||
rc = self.envfrom(self._sender)
|
|
||||||
self._stage = None
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# envrcpt
|
|
||||||
for rcpt in (rcpt,) + rcpts:
|
|
||||||
self._stage = Milter.M_ENVRCPT
|
|
||||||
rc = self.envrcpt('<%s>'%rcpt)
|
|
||||||
self._stage = None
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# data
|
|
||||||
self._stage = Milter.M_DATA
|
|
||||||
rc = self.data()
|
|
||||||
self._stage = None
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# header
|
|
||||||
for h,val in msg.items():
|
|
||||||
rc = self.header_bytes(h,val.encode())
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# eoh
|
|
||||||
self._stage = Milter.M_EOH
|
|
||||||
rc = self.eoh()
|
|
||||||
self._stage = None
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# body
|
|
||||||
header,body = msg.as_bytes().split(b'\n\n',1)
|
|
||||||
bfp = BytesIO(body)
|
|
||||||
while 1:
|
|
||||||
buf = bfp.read(8192)
|
|
||||||
if len(buf) == 0: break
|
|
||||||
rc = self.body(buf)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
self._msg = msg
|
|
||||||
self._body = BytesIO()
|
|
||||||
self._stage = Milter.M_EOM
|
|
||||||
rc = self.eom()
|
|
||||||
self._stage = None
|
|
||||||
if self._bodyreplaced:
|
|
||||||
body = self._body.getvalue()
|
|
||||||
self._body = BytesIO()
|
|
||||||
self._body.write(header)
|
|
||||||
self._body.write(b'\n\n')
|
|
||||||
self._body.write(body)
|
|
||||||
self.close()
|
|
||||||
self._close()
|
|
||||||
return rc
|
|
||||||
|
|
||||||
## Feed an email contained in a file to the %milter.
|
|
||||||
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
|
||||||
# @param sender MAIL FROM
|
|
||||||
# @param rcpts RCPT TO, multiple recipients may be supplied
|
|
||||||
def feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
|
||||||
with open('test/'+fname,'rb') as fp:
|
|
||||||
return self.feedFile(fp,sender,*rcpts)
|
|
||||||
|
|
||||||
## Call the connect and helo callbacks.
|
|
||||||
# The helo callback is not called if connect does not return CONTINUE.
|
|
||||||
# @param host the hostname passed to the connect callback
|
|
||||||
# @param helo the hostname passed to the helo callback
|
|
||||||
# @param ip the IP address passed to the connect callback
|
|
||||||
def connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
|
|
||||||
self._body = None
|
|
||||||
self._bodyreplaced = False
|
|
||||||
self._setctx(None)
|
|
||||||
opts = [ Milter.CURR_ACTS,~0,0,0 ]
|
|
||||||
self._stage = -1
|
|
||||||
rc = self.negotiate(opts)
|
|
||||||
self._stage = Milter.M_CONNECT
|
|
||||||
rc = super(TestBase,self).connect(host,1,(ip,1234))
|
|
||||||
if rc != Milter.CONTINUE:
|
|
||||||
self._stage = None
|
|
||||||
self.close()
|
|
||||||
return rc
|
|
||||||
self._stage = Milter.M_HELO
|
|
||||||
rc = self.hello(helo)
|
|
||||||
self._stage = None
|
|
||||||
if rc != Milter.CONTINUE:
|
|
||||||
self.close()
|
|
||||||
return rc
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
## @package Milter.testctx
|
|
||||||
# A test framework for milters that replaces milterContext rather
|
|
||||||
# than Milter.Base. Since miltermodule.c doesn't currently export
|
|
||||||
# a way to query callbacks set (and we might want to run without
|
|
||||||
# loading milter), we assume the callbacks set by Milter.runmilter().
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
from socket import AF_INET,AF_INET6
|
|
||||||
from sys import version as VERSION
|
|
||||||
import time
|
|
||||||
import mime
|
|
||||||
try:
|
|
||||||
from io import BytesIO
|
|
||||||
except:
|
|
||||||
from StringIO import StringIO as BytesIO
|
|
||||||
import Milter
|
|
||||||
from Milter import utils
|
|
||||||
|
|
||||||
## Milter context for unit testing %milter applications.
|
|
||||||
# A substitute for milter.milterContext that can be passed to
|
|
||||||
# Milter.Base._setctx().
|
|
||||||
# @since 1.0.3
|
|
||||||
class TestCtx(object):
|
|
||||||
default_opts = [Milter.CURR_ACTS,0x1fffff,0,0]
|
|
||||||
def __init__(self,logfile='test/milter.log'):
|
|
||||||
## Usually the Milter application derived from Milter.Base
|
|
||||||
self._priv = None
|
|
||||||
## List of recipients deleted
|
|
||||||
self._delrcpt = []
|
|
||||||
## List of recipients added
|
|
||||||
self._addrcpt = []
|
|
||||||
## Macros defined
|
|
||||||
self._macros = { }
|
|
||||||
## Reply codes and messages set by the %milter
|
|
||||||
self._reply = None
|
|
||||||
## The macros returned by protocol stage
|
|
||||||
self._symlist = [ None, None, None, None, None, None, None ]
|
|
||||||
## The message body.
|
|
||||||
self._body = None
|
|
||||||
## True if the %milter replaced the message body.
|
|
||||||
self._bodyreplaced = False
|
|
||||||
## True if the %milter changed any headers.
|
|
||||||
self._headerschanged = False
|
|
||||||
## The rfc822 message object for the current email being fed to the %milter.
|
|
||||||
self._msg = None
|
|
||||||
## The MAIL FROM for the current email being fed to the %milter
|
|
||||||
self._sender = None
|
|
||||||
## True if the %milter changed the envelope from.
|
|
||||||
self._envfromchanged = False
|
|
||||||
## List of recipients added
|
|
||||||
self._addrcpt = []
|
|
||||||
## Negotiated options
|
|
||||||
self._opts = TestCtx.default_opts
|
|
||||||
## Last activity
|
|
||||||
self._activity = time.time()
|
|
||||||
|
|
||||||
def getpriv(self):
|
|
||||||
return self._priv
|
|
||||||
|
|
||||||
def setpriv(self,priv):
|
|
||||||
self._priv = priv
|
|
||||||
|
|
||||||
def getsymval(self,name):
|
|
||||||
stage = self._stage
|
|
||||||
if stage >= 0:
|
|
||||||
try:
|
|
||||||
s = name.encode('utf8')
|
|
||||||
except: pass
|
|
||||||
syms = self._symlist[stage]
|
|
||||||
if syms is not None and s not in syms:
|
|
||||||
return None
|
|
||||||
return self._macros.get(name,None)
|
|
||||||
|
|
||||||
def _setsymval(self,name,val):
|
|
||||||
self._macros[name] = val
|
|
||||||
|
|
||||||
def setreply(self,rcode,xcode,*msg):
|
|
||||||
self._reply = (rcode,xcode) + msg
|
|
||||||
|
|
||||||
def setsymlist(self,stage,macros):
|
|
||||||
if self._stage != -1:
|
|
||||||
raise RuntimeError("setsymlist may only be called from negotiate")
|
|
||||||
# Records which macros are available to getsymval()
|
|
||||||
m = macros
|
|
||||||
try:
|
|
||||||
m = m.encode('utf8')
|
|
||||||
except: pass
|
|
||||||
try:
|
|
||||||
m = m.split(b' ')
|
|
||||||
except: pass
|
|
||||||
if len(m) > 5:
|
|
||||||
raise ValueError('setsymlist limited to 5 macros by MTA')
|
|
||||||
if self._symlist[stage] is not None:
|
|
||||||
raise ValueError('setsymlist already called for stage:'+stage)
|
|
||||||
if not m:
|
|
||||||
raise ValueError('setsymlist with empty list for stage:'+stage)
|
|
||||||
self._symlist[stage] = set(m)
|
|
||||||
|
|
||||||
def addheader(self,field,value,idx):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("addheader not called from eom()")
|
|
||||||
self._msg[field] = value
|
|
||||||
self._headerschanged = True
|
|
||||||
|
|
||||||
def chgheader(self,field,idx,value):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("chgheader not called from eom()")
|
|
||||||
if value == '':
|
|
||||||
del self._msg[field]
|
|
||||||
else:
|
|
||||||
self._msg[field] = value
|
|
||||||
self._headerschanged = True
|
|
||||||
|
|
||||||
def addrcpt(self,rcpt,params):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("addrcpt not called from eom()")
|
|
||||||
self._addrcpt.append((rcpt,params))
|
|
||||||
|
|
||||||
def delrcpt(self,rcpt):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("delrcpt not called from eom()")
|
|
||||||
self._delrcpt.append(rcpt)
|
|
||||||
|
|
||||||
def replacebody(self,chunk):
|
|
||||||
if self._body:
|
|
||||||
self._body.write(chunk)
|
|
||||||
self._bodyreplaced = True
|
|
||||||
else:
|
|
||||||
raise IOError("replacebody not called from eom()")
|
|
||||||
|
|
||||||
def chgfrom(self,sender,params=None):
|
|
||||||
if not self._body:
|
|
||||||
raise IOError("chgfrom not called from eom()")
|
|
||||||
self._envfromchanged = True
|
|
||||||
self._sender = sender
|
|
||||||
|
|
||||||
def quarantine(self,reason):
|
|
||||||
raise NotImplemented
|
|
||||||
|
|
||||||
## Reset activity timer.
|
|
||||||
def progress(self):
|
|
||||||
self._activity = time.time()
|
|
||||||
|
|
||||||
def _abort(self):
|
|
||||||
"What Milter sets for abort_callback"
|
|
||||||
self._priv.abort()
|
|
||||||
self._close()
|
|
||||||
|
|
||||||
def _close(self):
|
|
||||||
Milter.close_callback(self)
|
|
||||||
|
|
||||||
def _negotiate(self):
|
|
||||||
self._body = None
|
|
||||||
self._bodyreplaced = False
|
|
||||||
self._priv = None
|
|
||||||
self._opts = TestCtx.default_opts
|
|
||||||
self._stage = -1
|
|
||||||
rc = Milter.negotiate_callback(self,self._opts)
|
|
||||||
if rc == Milter.ALL_OPTS:
|
|
||||||
self._opts = TestCtx.default_opts
|
|
||||||
elif rc != Milter.CONTINUE:
|
|
||||||
self._abort()
|
|
||||||
self._close()
|
|
||||||
self._protocol = self._opts[1]
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def _connect(self,host='localhost',helo='spamrelay',ip='1.2.3.4'):
|
|
||||||
rc = self._negotiate()
|
|
||||||
# FIXME: what if not CONTINUE or ALL_OPTS?
|
|
||||||
if self._protocol & Milter.P_NOCONNECT:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
if utils.ip4re.match(ip):
|
|
||||||
af = AF_INET
|
|
||||||
elif utils.ip6re.match(ip):
|
|
||||||
af = AF_INET6
|
|
||||||
else:
|
|
||||||
raise ValueError('TestCtx.connect: invalid ip address: '+ip)
|
|
||||||
self._stage = Milter.M_CONNECT
|
|
||||||
rc = Milter.connect_callback(self,host,af,ip)
|
|
||||||
self._stage = None
|
|
||||||
if rc != Milter.CONTINUE:
|
|
||||||
self._close()
|
|
||||||
return rc
|
|
||||||
return self._helo(helo)
|
|
||||||
|
|
||||||
def _helo(self,helo):
|
|
||||||
if self._protocol & Milter.P_NOHELO:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
self._stage = Milter.M_HELO
|
|
||||||
rc = self._priv.hello(helo)
|
|
||||||
self._stage = None
|
|
||||||
if rc != Milter.CONTINUE:
|
|
||||||
self._close()
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def _envfrom(self,*s):
|
|
||||||
self._sender = s[0]
|
|
||||||
if self._protocol & Milter.P_NOMAIL:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
self._stage = Milter.M_ENVFROM
|
|
||||||
rc = self._priv.envfrom(*s)
|
|
||||||
self._stage = None
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def _envrcpt(self,s):
|
|
||||||
if self._protocol & Milter.P_NORCPT:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
self._stage = Milter.M_ENVRCPT
|
|
||||||
rc = self._priv.envrcpt(s)
|
|
||||||
self._stage = None
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def _data(self):
|
|
||||||
if self._protocol & Milter.P_NODATA:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
self._stage = Milter.M_DATA
|
|
||||||
rc = self._priv.data()
|
|
||||||
self._stage = None
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def _header(self,fld,val):
|
|
||||||
if VERSION < '3.0.0':
|
|
||||||
return self._priv.header(fld,val)
|
|
||||||
# email.message_from_binary_file uses surrogateescape to
|
|
||||||
# preserve original bytes in unicode string for decoding errors.
|
|
||||||
# convert str or Header back to original bytes
|
|
||||||
if hasattr(val, '_chunks'):
|
|
||||||
# val is a Header object for invalid header values
|
|
||||||
v = b''
|
|
||||||
for s,charset in val._chunks:
|
|
||||||
# recover the original bytes
|
|
||||||
b = s.encode(encoding='ascii',errors='surrogateescape')
|
|
||||||
v += b
|
|
||||||
else:
|
|
||||||
v = val.encode(encoding='ascii',errors='surrogateescape')
|
|
||||||
# invoke the Milter header_callback
|
|
||||||
return self._priv.header_bytes(fld,v)
|
|
||||||
|
|
||||||
def _eoh(self):
|
|
||||||
if self._protocol & Milter.P_NOEOH:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
self._stage = Milter.M_EOH
|
|
||||||
rc = self._priv.eoh()
|
|
||||||
self._stage = None
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def _feed_body(self,bfp):
|
|
||||||
if self._protocol & Milter.P_NOBODY:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
while True:
|
|
||||||
buf = bfp.read(8192)
|
|
||||||
if len(buf) == 0: break
|
|
||||||
rc = self._priv.body(buf)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def _eom(self):
|
|
||||||
self._body = BytesIO()
|
|
||||||
self._stage = Milter.M_EOM
|
|
||||||
rc = self._priv.eom()
|
|
||||||
self._stage = None
|
|
||||||
return rc
|
|
||||||
|
|
||||||
## Feed a file like object to the ctx. Calls the callbacks in
|
|
||||||
# the same sequence as libmilter.
|
|
||||||
# @param fp the file with rfc2822 message stream
|
|
||||||
# @param sender the MAIL FROM
|
|
||||||
# @param rcpt RCPT TO - additional recipients may follow
|
|
||||||
def _feedFile(self,fp,sender="spam@adv.com",rcpt="victim@lamb.com",*rcpts):
|
|
||||||
self._body = None
|
|
||||||
self._bodyreplaced = False
|
|
||||||
self._headerschanged = False
|
|
||||||
self._reply = None
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
self._msg = msg
|
|
||||||
# envfrom
|
|
||||||
rc = self._envfrom('<%s>'%sender)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# envrcpt
|
|
||||||
for rcpt in (rcpt,) + rcpts:
|
|
||||||
rc = self._envrcpt('<%s>'%rcpt)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# data
|
|
||||||
rc = self._data()
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# header
|
|
||||||
for h,val in msg.items():
|
|
||||||
rc = self._header(h,val)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# eoh
|
|
||||||
rc = self._eoh()
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
# body
|
|
||||||
header,body = msg.as_bytes().split(b'\n\n',1)
|
|
||||||
rc = self._feed_body(BytesIO(body))
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
rc = self._eom()
|
|
||||||
if self._bodyreplaced:
|
|
||||||
body = self._body.getvalue()
|
|
||||||
self._body = BytesIO()
|
|
||||||
self._body.write(header)
|
|
||||||
self._body.write(b'\n\n')
|
|
||||||
self._body.write(body)
|
|
||||||
return rc
|
|
||||||
|
|
||||||
## Feed an email contained in a file to the %milter.
|
|
||||||
# This is a convenience method that invokes @link #feedFile feedFile @endlink.
|
|
||||||
# @param sender MAIL FROM
|
|
||||||
# @param rcpts RCPT TO, multiple recipients may be supplied
|
|
||||||
def _feedMsg(self,fname,sender="spam@adv.com",*rcpts):
|
|
||||||
with open('test/'+fname,'rb') as fp:
|
|
||||||
return self._feedFile(fp,sender,*rcpts)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2005 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
|
|
||||||
# The localpart of SMTP return addresses is often signed. The format
|
|
||||||
# of the signing is application specific and doesn't concern us -
|
|
||||||
# except that we wish to extract some sort of fixed string from
|
|
||||||
# the variable signature which represents the "source" of the message.
|
|
||||||
|
|
||||||
def unsign(s):
|
|
||||||
"""Attempt to unsign localpart and return original email.
|
|
||||||
No attempt is made to verify the signature.
|
|
||||||
>>> unsign('SRS0=8Y3CZ=3U=jsconnor.com=bills@bmsi.com')
|
|
||||||
'bills@jsconnor.com'
|
|
||||||
"""
|
|
||||||
# not implemented yet
|
|
||||||
return s
|
|
||||||
-229
@@ -1,229 +0,0 @@
|
|||||||
## @package Milter.utils
|
|
||||||
# Miscellaneous functions.
|
|
||||||
#
|
|
||||||
|
|
||||||
import re
|
|
||||||
import struct
|
|
||||||
import socket
|
|
||||||
import email.errors
|
|
||||||
from email.header import decode_header
|
|
||||||
import email.base64mime
|
|
||||||
import email.utils
|
|
||||||
from fnmatch import fnmatchcase
|
|
||||||
from binascii import a2b_base64
|
|
||||||
|
|
||||||
dnsre = re.compile(r'^[a-z][-a-z\d.]+$', re.IGNORECASE)
|
|
||||||
PAT_IP4 = r'\.'.join([r'(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])']*4)
|
|
||||||
ip4re = re.compile(PAT_IP4+'$')
|
|
||||||
ip6re = re.compile( '(?:%(hex4)s:){6}%(ls32)s$'
|
|
||||||
'|::(?:%(hex4)s:){5}%(ls32)s$'
|
|
||||||
'|(?:%(hex4)s)?::(?:%(hex4)s:){4}%(ls32)s$'
|
|
||||||
'|(?:(?:%(hex4)s:){0,1}%(hex4)s)?::(?:%(hex4)s:){3}%(ls32)s$'
|
|
||||||
'|(?:(?:%(hex4)s:){0,2}%(hex4)s)?::(?:%(hex4)s:){2}%(ls32)s$'
|
|
||||||
'|(?:(?:%(hex4)s:){0,3}%(hex4)s)?::%(hex4)s:%(ls32)s$'
|
|
||||||
'|(?:(?:%(hex4)s:){0,4}%(hex4)s)?::%(ls32)s$'
|
|
||||||
'|(?:(?:%(hex4)s:){0,5}%(hex4)s)?::%(hex4)s$'
|
|
||||||
'|(?:(?:%(hex4)s:){0,6}%(hex4)s)?::$'
|
|
||||||
% {
|
|
||||||
'ls32': r'(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|%s)'%PAT_IP4,
|
|
||||||
'hex4': r'[0-9a-f]{1,4}'
|
|
||||||
}, re.IGNORECASE)
|
|
||||||
|
|
||||||
# from spf.py
|
|
||||||
def addr2bin(s):
|
|
||||||
"""Convert a string IPv4 address into an unsigned integer."""
|
|
||||||
if s.find(':') >= 0:
|
|
||||||
try:
|
|
||||||
return bin2long6(inet_pton(s))
|
|
||||||
except:
|
|
||||||
raise socket.error("Invalid IP6 address: "+s)
|
|
||||||
try:
|
|
||||||
return struct.unpack("!L", socket.inet_aton(s))[0]
|
|
||||||
except socket.error:
|
|
||||||
raise socket.error("Invalid IP4 address: "+s)
|
|
||||||
|
|
||||||
def bin2long6(s):
|
|
||||||
"""Convert binary IP6 address into an unsigned Python long integer."""
|
|
||||||
h, l = struct.unpack("!QQ", s)
|
|
||||||
return h << 64 | l
|
|
||||||
|
|
||||||
if hasattr(socket,'has_ipv6') and socket.has_ipv6:
|
|
||||||
def inet_ntop(s):
|
|
||||||
return socket.inet_ntop(socket.AF_INET6,s)
|
|
||||||
def inet_pton(s):
|
|
||||||
return socket.inet_pton(socket.AF_INET6,s.strip())
|
|
||||||
else:
|
|
||||||
from pyip6 import inet_ntop, inet_pton
|
|
||||||
|
|
||||||
MASK = 0xFFFFFFFF
|
|
||||||
MASK6 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
|
||||||
|
|
||||||
def cidr(i,n,mask=MASK):
|
|
||||||
return ~(mask >> n) & mask & i
|
|
||||||
|
|
||||||
def iniplist(ipaddr,iplist):
|
|
||||||
"""Return whether ip is in cidr list
|
|
||||||
>>> iniplist('66.179.26.146',['127.0.0.1','66.179.26.128/26'])
|
|
||||||
True
|
|
||||||
>>> iniplist('127.0.0.1',['127.0.0.1','66.179.26.128/26'])
|
|
||||||
True
|
|
||||||
>>> iniplist('192.168.0.45',['192.168.0.*'])
|
|
||||||
True
|
|
||||||
>>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
|
|
||||||
True
|
|
||||||
>>> iniplist('2606:2800:220:1::',['example.com/40'])
|
|
||||||
True
|
|
||||||
>>> iniplist('4.2.2.2',['nothing.example.com'])
|
|
||||||
False
|
|
||||||
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
|
||||||
True
|
|
||||||
>>> iniplist('2G01:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
ValueError: Invalid ip syntax:2G01:610:779:0:223:6cff:fe9a:9cf3
|
|
||||||
"""
|
|
||||||
if ip4re.match(ipaddr):
|
|
||||||
fam = socket.AF_INET
|
|
||||||
ipnum = addr2bin(ipaddr)
|
|
||||||
elif ip6re.match(ipaddr):
|
|
||||||
fam = socket.AF_INET6
|
|
||||||
ipnum = bin2long6(inet_pton(ipaddr))
|
|
||||||
else:
|
|
||||||
raise ValueError('Invalid ip syntax:'+ipaddr)
|
|
||||||
for pat in iplist:
|
|
||||||
p = pat.split('/',1)
|
|
||||||
if ip4re.match(p[0]):
|
|
||||||
if len(p) > 1:
|
|
||||||
n = int(p[1])
|
|
||||||
else:
|
|
||||||
n = 32
|
|
||||||
if cidr(addr2bin(p[0]),n) == cidr(ipnum,n):
|
|
||||||
return True
|
|
||||||
elif ip6re.match(p[0]):
|
|
||||||
if len(p) > 1:
|
|
||||||
n = int(p[1])
|
|
||||||
else:
|
|
||||||
n = 128
|
|
||||||
if cidr(bin2long6(inet_pton(p[0])),n,MASK6) == cidr(ipnum,n,MASK6):
|
|
||||||
return True
|
|
||||||
elif dnsre.match(p[0]):
|
|
||||||
try:
|
|
||||||
sfx = '/'.join(['']+p[1:])
|
|
||||||
addrlist = [r[4][0]+sfx for r in socket.getaddrinfo(p[0],25,fam)]
|
|
||||||
if iniplist(ipaddr,addrlist):
|
|
||||||
return True
|
|
||||||
except socket.gaierror: pass
|
|
||||||
elif fnmatchcase(ipaddr,pat):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
## Split email into Fullname and address.
|
|
||||||
# This replaces <code>email.utils.parseaddr</code> but fixes
|
|
||||||
# some <a href="http://bugs.python.org/issue1025395">tricky test cases</a>.
|
|
||||||
# Additional tricky cases are still broken. Patches welcome.
|
|
||||||
#
|
|
||||||
def parseaddr(t):
|
|
||||||
"""Split email into Fullname and address.
|
|
||||||
|
|
||||||
>>> parseaddr('user@example.com')
|
|
||||||
('', 'user@example.com')
|
|
||||||
>>> parseaddr('"Full Name" <foo@example.com>')
|
|
||||||
('Full Name', 'foo@example.com')
|
|
||||||
>>> parseaddr('spam@spammer.com <foo@example.com>')
|
|
||||||
('spam@spammer.com', 'foo@example.com')
|
|
||||||
>>> parseaddr('God@heaven <@hop1.org,@hop2.net:jeff@spec.org>')
|
|
||||||
('God@heaven', 'jeff@spec.org')
|
|
||||||
>>> parseaddr('Real Name ((comment)) <addr...@example.com>')
|
|
||||||
('Real Name (comment)', 'addr...@example.com')
|
|
||||||
"""
|
|
||||||
#return email.utils.parseaddr(t)
|
|
||||||
res = email.utils.parseaddr(t)
|
|
||||||
# dirty fix for some broken cases
|
|
||||||
if not res[0]:
|
|
||||||
pos = t.find('<')
|
|
||||||
if pos > 0 and t[-1] == '>':
|
|
||||||
addrspec = t[pos+1:-1]
|
|
||||||
pos1 = addrspec.rfind(':')
|
|
||||||
if pos1 > 0:
|
|
||||||
addrspec = addrspec[pos1+1:]
|
|
||||||
return email.utils.parseaddr('"%s" <%s>' % (t[:pos].strip(),addrspec))
|
|
||||||
if not res[1]:
|
|
||||||
pos = t.find('<')
|
|
||||||
if pos > 0 and t[-1] == '>':
|
|
||||||
addrspec = t[pos+1:-1]
|
|
||||||
pos1 = addrspec.rfind(':')
|
|
||||||
if pos1 > 0:
|
|
||||||
addrspec = addrspec[pos1+1:]
|
|
||||||
return email.utils.parseaddr('%s<%s>' % (t[:pos].strip(),addrspec))
|
|
||||||
return res
|
|
||||||
|
|
||||||
## Fix email.base64mime.decode to add any missing padding
|
|
||||||
def decode(s, convert_eols=None):
|
|
||||||
if not s: return s
|
|
||||||
while len(s) % 4: s += '=' # add missing padding
|
|
||||||
dec = a2b_base64(s)
|
|
||||||
if convert_eols:
|
|
||||||
return dec.replace(CRLF, convert_eols)
|
|
||||||
return dec
|
|
||||||
|
|
||||||
email.base64mime.decode = decode
|
|
||||||
|
|
||||||
def parse_addr(t):
|
|
||||||
"""Split email into user,domain.
|
|
||||||
|
|
||||||
>>> parse_addr('user@example.com')
|
|
||||||
['user', 'example.com']
|
|
||||||
>>> parse_addr('"user@example.com"')
|
|
||||||
['user@example.com']
|
|
||||||
>>> parse_addr('"user@bar"@example.com')
|
|
||||||
['user@bar', 'example.com']
|
|
||||||
>>> parse_addr('foo')
|
|
||||||
['foo']
|
|
||||||
>>> parse_addr('@mx.example.com:user@example.com')
|
|
||||||
['user', 'example.com']
|
|
||||||
>>> parse_addr('@user@example.com')
|
|
||||||
['@user', 'example.com']
|
|
||||||
"""
|
|
||||||
if t.startswith('<') and t.endswith('>'): t = t[1:-1]
|
|
||||||
if t.startswith('"'):
|
|
||||||
if t.endswith('"'): return [t[1:-1]]
|
|
||||||
pos = t.find('"@')
|
|
||||||
if pos > 0: return [t[1:pos],t[pos+2:]]
|
|
||||||
if t.startswith('@'):
|
|
||||||
try: t = t.split(':',1)[1]
|
|
||||||
except IndexError: pass
|
|
||||||
return t.rsplit('@',1)
|
|
||||||
|
|
||||||
## Decode headers gratuitously encoded to hide the content.
|
|
||||||
# Spammers often encode headers to obscure the content from
|
|
||||||
# spam filters. This function decodes gratuitously encoded
|
|
||||||
# headers.
|
|
||||||
# @param val the raw header value
|
|
||||||
# @return the decoded value or the original raw value
|
|
||||||
|
|
||||||
def parse_header(val):
|
|
||||||
"""Decode headers gratuitously encoded to hide the content.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
h = decode_header(val)
|
|
||||||
if not len(h) or (not h[0][1] and len(h) == 1): return val
|
|
||||||
u = []
|
|
||||||
for s,enc in h:
|
|
||||||
if enc:
|
|
||||||
try:
|
|
||||||
u.append(s.decode(enc,'replace'))
|
|
||||||
except LookupError:
|
|
||||||
u.append(s.decode())
|
|
||||||
else:
|
|
||||||
u.append(s.decode())
|
|
||||||
u = ''.join(u)
|
|
||||||
if type(u) is str: return u
|
|
||||||
for enc in ('us-ascii','iso-8859-1','utf-8'):
|
|
||||||
try:
|
|
||||||
return u.encode(enc)
|
|
||||||
except UnicodeError: continue
|
|
||||||
except UnicodeDecodeError: pass
|
|
||||||
except LookupError: pass
|
|
||||||
except ValueError: pass
|
|
||||||
except email.errors.HeaderParseError: pass
|
|
||||||
return val
|
|
||||||
@@ -1,67 +1,5 @@
|
|||||||
See pymilter.spec for recent history.
|
Here is a history of user visible changes to Python milter.
|
||||||
|
|
||||||
Here is a history of older changes to Python milter.
|
|
||||||
0.8.8 move AddrCache, parse_addr, iniplist, parse_header to Milter package
|
|
||||||
fix plock for missing source and can't change owner/group
|
|
||||||
add sample spfmilter.py milter
|
|
||||||
private_relay config option
|
|
||||||
0.8.7 Move spf module to pyspf
|
|
||||||
Prevent PTR cache poisoning
|
|
||||||
More lame bounce heuristics
|
|
||||||
Do plain CBV when template is missing
|
|
||||||
0.8.6 Support CBV timeout
|
|
||||||
Support fail template, headers in templates
|
|
||||||
Create GOSSiP record only when connection will procede to DATA.
|
|
||||||
More SPF lax heuristics
|
|
||||||
Don't require SPF pass for white/black listing mail from trusted relay.
|
|
||||||
Support localpart wildcard for white and black lists.
|
|
||||||
Delay reject of unsigned RCPT for postmaster and abuse only
|
|
||||||
Fix dsn reporting of hard permerror
|
|
||||||
Resolve FIXME for wrap_close in miltermodule.c
|
|
||||||
Add Message-ID to DSNs
|
|
||||||
Use signed Message-ID in delayed reject to blacklist senders
|
|
||||||
Auto-train via blacklist and auto-whitelist
|
|
||||||
Don't check userlist for signed MFROM
|
|
||||||
Accept but skip DSPAM training for whitelisted senders without SPF PASS
|
|
||||||
Report GC stats
|
|
||||||
Support CIDR matching for IP lists
|
|
||||||
Support pysrs sign feature
|
|
||||||
Support localpart specific SPF policy in access file
|
|
||||||
0.8.5 Simple trusted_forwarder implementation.
|
|
||||||
Fix access_file neutral policy
|
|
||||||
Move Received-SPF header to beginning of headers
|
|
||||||
Supply keyword info for all results in Received-SPF header.
|
|
||||||
Move guessed SPF result to separate header
|
|
||||||
Activate smfi_insheader only when SMFIR_INSHEADER defined
|
|
||||||
Handle NULL MX in spf.py
|
|
||||||
in-process GOSSiP server support (to be extended later)
|
|
||||||
Expire CBV cache and renew auto-whitelist entries
|
|
||||||
0.8.4 Auto-whitelist recipients of outgoing email.
|
|
||||||
Fix SPF policy via sendmail access map (case insensitive keys).
|
|
||||||
Train screener on whitelisted messages
|
|
||||||
Optional idx parameter to addheader to invoke smfi_insheader
|
|
||||||
Activate progress API when SMFIR_PROGRESS defined
|
|
||||||
0.8.3 Keep screened honeypot mail, but optionally discard honeypot only mail.
|
|
||||||
spf_accept_fail option for braindead SPF senders
|
|
||||||
(treats fail like softfail)
|
|
||||||
Option to set SPF policy via sendmail access map.
|
|
||||||
Option to supply Sender header from MAIL FROM when missing.
|
|
||||||
Consider SMTP AUTH connections internal.
|
|
||||||
Send DSN for SPF errors corrected by extended processing.
|
|
||||||
Send DSN before SCREENED mail is quarantined
|
|
||||||
Use logging package to keep log lines atomic.
|
|
||||||
0.8.2 Strict processing limits per SPF RFC
|
|
||||||
Fixed several parsing bugs under RFC
|
|
||||||
Support official IANA SPF record (type99)
|
|
||||||
Honeypot support (requires pydspam-1.1.9)
|
|
||||||
Extended SPF processing results beyond strict RFC limits
|
|
||||||
Support original SES for bounce protection (requires pysrs-0.30.10)
|
|
||||||
Callback exception processing option in milter module
|
|
||||||
Handle corrupt ZIP attachments
|
|
||||||
0.8.1 Fix zip in zip loop in mime.py
|
|
||||||
Fix HeaderParseError in bms.py header callback
|
|
||||||
Check internal_domains for outgoing mail
|
|
||||||
Fix inconsistent results from send_dsn
|
|
||||||
0.8.0 Move Milter module to subpackage.
|
0.8.0 Move Milter module to subpackage.
|
||||||
DSN support for Three strikes rule and SPF SOFTFAIL
|
DSN support for Three strikes rule and SPF SOFTFAIL
|
||||||
Move /*mime*/ and dynip to Milter subpackage
|
Move /*mime*/ and dynip to Milter subpackage
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
# Abstract
|
|
||||||
|
|
||||||
This is a python extension module to enable python scripts to attach to
|
|
||||||
Sendmail's libmilter API, enabling filtering of messages as they arrive.
|
|
||||||
Since it's a script, you can do anything you want to the message - screen
|
|
||||||
out viruses, collect statistics, add or modify headers, etc. You can, at
|
|
||||||
any point, tell Sendmail to reject, discard, or accept the message.
|
|
||||||
|
|
||||||
Additional python modules provide for navigating and modifying MIME parts, and
|
|
||||||
sending DSNs or doing CBVs.
|
|
||||||
|
|
||||||
# Requirements
|
|
||||||
|
|
||||||
Python milter extension: https://pypi.org/project/pymilter/
|
|
||||||
Python: http://www.python.org
|
|
||||||
Sendmail: http://www.sendmail.org
|
|
||||||
or
|
|
||||||
Postfix: http://www.postfix.org/MILTER_README.html
|
|
||||||
|
|
||||||
# Quick Installation
|
|
||||||
|
|
||||||
1. Build and install Sendmail, enabling libmilter (see libmilter/README).
|
|
||||||
2. Build and install Python, enabling threading.
|
|
||||||
3. Install this module: python setup.py --help
|
|
||||||
4. Add these two lines to sendmail.cf[a]:
|
|
||||||
```
|
|
||||||
O InputMailFilters=pythonfilter
|
|
||||||
Xpythonfilter, S=local:/home/username/pythonsock
|
|
||||||
```
|
|
||||||
5. Run the sample.py example milter with: python sample.py
|
|
||||||
Note that milters should almost certainly not run as root.
|
|
||||||
|
|
||||||
That's it. Incoming mail will cause the milter to print some things, and
|
|
||||||
some email will be rejected (see the "header" method). Edit and play.
|
|
||||||
See spfmilter.py for a functional SPF milter, or see bms.py for an complex
|
|
||||||
milter used in production.
|
|
||||||
|
|
||||||
[a] This is for a quick test. Your sendmail.cf in most distros will get
|
|
||||||
overwritten whenever sendmail.mc is updated. To make a milter permanent,
|
|
||||||
add something like:
|
|
||||||
```
|
|
||||||
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock, F=T, T=C:5m;S:20s;R:5m;E:5m')
|
|
||||||
```
|
|
||||||
to sendmail.mc instead.
|
|
||||||
|
|
||||||
# Not-so-quick Installation
|
|
||||||
|
|
||||||
First install Sendmail. Make sure you read libmilter/README in the Sendmail
|
|
||||||
source directory, and make sure you enable libmilter before you build. The
|
|
||||||
8.11 series had libmilter marked as FFR (For Future Release); 8.12
|
|
||||||
officially supports libmilter, but it's still not built by default.
|
|
||||||
|
|
||||||
Install Python, and enable threading in Modules/Setup.
|
|
||||||
|
|
||||||
Install this miltermodule package; DistUtils Automatic Installation:
|
|
||||||
|
|
||||||
$ python setup.py --help
|
|
||||||
|
|
||||||
Now that everything is installed, we need to tell sendmail that we're going
|
|
||||||
to filter incoming email. Add lines similar to the following to
|
|
||||||
sendmail.cf:
|
|
||||||
```
|
|
||||||
O InputMailFilters=pythonfilter
|
|
||||||
Xpythonfilter, S=local:/home/username/pythonsock
|
|
||||||
```
|
|
||||||
The "O" line tells sendmail which filters to use in what order; here we're
|
|
||||||
telling sendmail to use the filter named "pythonfilter".
|
|
||||||
|
|
||||||
The next line, the "X" line (for "eXternal"), lists that filter along with
|
|
||||||
some options associated with it. In this case, we have the "S" option, which
|
|
||||||
names the socket that sendmail will use to communicate with this particular
|
|
||||||
milter. This milter's socket is a unix-domain socket in the filesystem.
|
|
||||||
See libmilter/README for the definitive list of options.
|
|
||||||
|
|
||||||
NB: The name is specified in two places: here, in sendmail's cf file, and
|
|
||||||
in the milter itself. Make sure the two match.
|
|
||||||
|
|
||||||
NB: The above lines can be added in your .mc file with this line:
|
|
||||||
```
|
|
||||||
INPUT_MAIL_FILTER(`pythonfilter', `S=local:/home/username/pythonsock')
|
|
||||||
```
|
|
||||||
For versions of sendmail prior to 8.12, you will need to enable
|
|
||||||
`_FFR_MILTER` for the cf macros. For example,
|
|
||||||
```
|
|
||||||
m4 -D_FFR_MILTER ../m4/cf.m4 myconfig.mc > myconfig.cf
|
|
||||||
```
|
|
||||||
# IPv6 Notes
|
|
||||||
|
|
||||||
The IPv6 protocol is supported if your operation system supports it
|
|
||||||
and if sendmail was compiled with IPv6 support. To determine if your
|
|
||||||
sendmail supports IPv6, run "sendmail -d0" and check for the NETINET6
|
|
||||||
compilation option. To compile sendmail with IPv6 support, add this
|
|
||||||
declaration to your site.config.m4 before building it:
|
|
||||||
```
|
|
||||||
APPENDDEF(`confENVDEF', `-DNETINET6=1')
|
|
||||||
```
|
|
||||||
IPv6 support can show up in two places; the communications socket
|
|
||||||
between the milter and sendmail processes and in the host address
|
|
||||||
argument to the connect() callback method.
|
|
||||||
|
|
||||||
For sendmail to be able to accept IPv6 SMTP sessions, you must
|
|
||||||
configure the daemon to listen on an IPv6 port. Furthermore if you
|
|
||||||
want to allow both IPv4 and IPv6 connections, some operating systems
|
|
||||||
will require that each listens to different port numbers. For an
|
|
||||||
IPv6-only setup, your sendmail configuration should contain a line
|
|
||||||
similar to (first line is for sendmail.mc, second is sendmail.cf):
|
|
||||||
```
|
|
||||||
DAEMON_OPTIONS(`Name=MTA-v6, Family=inet6, Modify=C, Port=25')
|
|
||||||
O DaemonPortOptions=Name=MTA-v6, Family=inet6, Modify=C, Port=25
|
|
||||||
```
|
|
||||||
To allow sendmail and the milter process to communicate with each
|
|
||||||
other over IPv6, you may use the "inet6" socket name prefix, as in:
|
|
||||||
```
|
|
||||||
Xpythonfilter, S=inet6:1234@fec0:0:0:7::5c
|
|
||||||
```
|
|
||||||
The connect() callback method in the milter class will pass the
|
|
||||||
IPv6-specific information in the 'hostaddr' argument as a tuple. Note
|
|
||||||
that the type of this value is dependent upon the protocol family, and
|
|
||||||
is not compatible with IPv4 connections. Therefore you should always
|
|
||||||
check the family argument before attempting to use the hostaddr
|
|
||||||
argument. A quick example showing this follows:
|
|
||||||
```
|
|
||||||
import socket
|
|
||||||
|
|
||||||
class ipv6awareMilter(Milter.Milter):
|
|
||||||
|
|
||||||
def connect(self,hostname,family,hostaddr):
|
|
||||||
if family==socket.AF_INET:
|
|
||||||
ipaddress, port = hostaddr
|
|
||||||
elif family==socket.AF_INET6:
|
|
||||||
ip6address, port, flowinfo, scopeid = hostaddr
|
|
||||||
elif family==socket.AF_UNIX:
|
|
||||||
socketpath = hostaddr
|
|
||||||
```
|
|
||||||
The hostname argument is always safe to use without interpreting the
|
|
||||||
protocol family. For IPv6 connections for which the hostname can not
|
|
||||||
be determined the hostname will appear similar to the string
|
|
||||||
"[IPv6:::1]" with the corresponding hostaddr[0] being "::1". Refer to
|
|
||||||
RFC 2553 for information on interpreting and using the flowinfo and
|
|
||||||
scopeid socket attributes, both of which are integers.
|
|
||||||
|
|
||||||
# Authors
|
|
||||||
|
|
||||||
Jim Niemira (urmane@urmane.org) wrote the original C module and some quick
|
|
||||||
and dirty python to use it. Stuart D. Gathman (stuart@gathman.org) took that
|
|
||||||
kludge and added threading and context objects to it, wrote a proper OO
|
|
||||||
wrapper (Milter.py) that handles attachments, did lots of testing, packaged
|
|
||||||
it with distutils, and generally transformed it from a quick hack to a
|
|
||||||
real, usable Python extension.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
Test case for Milter/dsn.py
|
|
||||||
|
|
||||||
Lookup exact RFC syntax of real name / email and make
|
|
||||||
Milter.utils.parse_addr() pass all unit tests.
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
## @mainpage Writing Milters in Python
|
|
||||||
#
|
|
||||||
# At the lowest level, the <code>milter</code> module provides a thin wrapper
|
|
||||||
# around the <a href="milter_api/index.html"> sendmail
|
|
||||||
# libmilter API</a>. This API lets you register callbacks for a number of
|
|
||||||
# events in the process of sendmail receiving a message via SMTP. These
|
|
||||||
# events include the initial connection from a MTA, the envelope sender and
|
|
||||||
# recipients, the top level mail headers, and the message body. There are
|
|
||||||
# options to mangle all of these components of the message as it passes through
|
|
||||||
# the %milter.
|
|
||||||
#
|
|
||||||
# At the next level, the <code>Milter</code> module (note the case difference)
|
|
||||||
# provides a Python friendly object oriented wrapper for the low level API. To
|
|
||||||
# use the Milter module, an application registers a 'factory' to create an
|
|
||||||
# object for each connection from a MTA to sendmail. These connection objects
|
|
||||||
# must provide methods corresponding to the libmilter event callbacks.
|
|
||||||
#
|
|
||||||
# Each callback method returns a code to tell sendmail whether to proceed with
|
|
||||||
# processing the message. This is a big advantage of milters over other mail
|
|
||||||
# filtering systems. Unwanted mail can be stopped in its tracks at the
|
|
||||||
# earliest possible point. The callback return codes are
|
|
||||||
# milter.CONTINUE, milter.REJECT, milter.DISCARD, milter.ACCEPT,
|
|
||||||
# milter.TEMPFAIL, milter.SKIP, milter.NOREPLY.
|
|
||||||
#
|
|
||||||
# The Milter.Base class provides default implementations for
|
|
||||||
# event methods that do nothing, and also provides wrappers for the libmilter
|
|
||||||
# methods to mutate the message. It automatically negotiates with MTA
|
|
||||||
# which protocol steps need to be processed by the %milter, based on
|
|
||||||
# which callback methods are overridden.
|
|
||||||
#
|
|
||||||
# The Milter.Milter class provides an alternate default
|
|
||||||
# implementation that logs the main milter callbacks, but otherwise does
|
|
||||||
# nothing. It is provided for compatibility.
|
|
||||||
#
|
|
||||||
# The mime module provides a wrapper for the Python email package
|
|
||||||
# that fixes some bugs, and simplifies modifying selected parts of a MIME
|
|
||||||
# message.
|
|
||||||
#
|
|
||||||
# @section threading
|
|
||||||
#
|
|
||||||
# The libmilter library which pymilter wraps
|
|
||||||
# <a href="milter_overview#SignalHandling">handles
|
|
||||||
# all signals</a> itself, and expects to be called from a single main thread.
|
|
||||||
# It handles SIGTERM, SIGHUP, and SIGINT, mapping the first two to
|
|
||||||
# <a href="milter_api/smfi_stop.html">smfi_stop</a>
|
|
||||||
# and the last to an internal ABORT.
|
|
||||||
#
|
|
||||||
# If you use python threads or threading modules, then signal handling gets
|
|
||||||
# confused. Threads may still be useful, but you may need to provide an
|
|
||||||
# alternate means of causing graceful shutdown.
|
|
||||||
#
|
|
||||||
# You may find the
|
|
||||||
# <a href="http://docs.python.org/release/2.6.6/library/multiprocessing.html">
|
|
||||||
# multiprocessing</a> module useful. It can be a drop-in
|
|
||||||
# replacement for threading as illustrated in
|
|
||||||
# <a href="milter-template_8py-example.html">milter-template.py</a>.
|
|
||||||
#
|
|
||||||
# @section Useful python packages for milters
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/sdgathman/pymilter">pymilter</a> - this package.
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/sdgathman/pyspf">pyspf</a> checks the
|
|
||||||
# SMTP envelope sender (MAIL FROM, passed to the Milter.Base.envfrom callback)
|
|
||||||
# against a Sender Policy published in DNS by the sending domain. This
|
|
||||||
# can prevent forgery of the MAIL FROM. SPF is Sender Policy Framework.
|
|
||||||
#
|
|
||||||
# <a href="https://launchpad.net/dkimpy">pydkim</a> checks a DKIM signature
|
|
||||||
# of the email body and headers against a public key published in DNS by
|
|
||||||
# the signing domain. DKIM is DomainKeys Identified Mail.
|
|
||||||
#
|
|
||||||
# The <a href="https://pypi.python.org/pypi/authres/">authres</a> module
|
|
||||||
# parses and formats the Authentication-Results email header, providing
|
|
||||||
# a standard place to summarize the results from DKIM, SPF, rDNS, SMTP AUTH,
|
|
||||||
# and other email authentication methods.
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/sdgathman/pydspam/">pydspam</a> wraps
|
|
||||||
# the libdspam API of the <a href="http://dspam.sourceforge.net/">DSPAM</a>
|
|
||||||
# project.
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/sdgathman/pysrs/">pysrs</a> rewrites
|
|
||||||
# MAIL FROM to include a timestamped signature so that "bounce spam"
|
|
||||||
# can be immediately rejected.
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/sdgathman/pygossip/">pygossip</a> is a
|
|
||||||
# system to track reputation by domain and authentication level and type,
|
|
||||||
# and a simple protocol to gossip about reputations with other mail servers.
|
|
||||||
#
|
|
||||||
# @section Milters written with pymilter
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/croessner/vrfydmn">Verify Domain</a> is a
|
|
||||||
# Postfix milter that rejects/fixes manipulated From: header
|
|
||||||
# on a mail host with multiple virtual domains.
|
|
||||||
#
|
|
||||||
# <a href="https://github.com/sdgathman/milter/">BMS Milter</a> has several
|
|
||||||
# milters, a big complicated spam filter that integrates multiple
|
|
||||||
# authentication protocols with pydspam, and two simple ones: spfmilter.py and
|
|
||||||
# dkim-milter.py.
|
|
||||||
#
|
|
||||||
-251
@@ -1,251 +0,0 @@
|
|||||||
# Document miltermodule for Doxygen
|
|
||||||
#
|
|
||||||
|
|
||||||
## @package milter
|
|
||||||
#
|
|
||||||
# A thin wrapper around libmilter. Most users will not import
|
|
||||||
# milter directly, but will instead import Milter and subclass
|
|
||||||
# Milter.Base. This module gives you ultimate low level control
|
|
||||||
# from python.
|
|
||||||
#
|
|
||||||
|
|
||||||
## Continue processing the current connection, message, or recipient.
|
|
||||||
CONTINUE = 0
|
|
||||||
## For a connection-oriented routine, reject this connection;
|
|
||||||
# call Milter.Base.close(). For a message-oriented routine, except
|
|
||||||
# Milter.Base.eom() or Milter.Base.abort(), reject this message. For a
|
|
||||||
# recipient-oriented routine, reject the current recipient (but continue
|
|
||||||
# processing the current message).
|
|
||||||
REJECT = 1
|
|
||||||
|
|
||||||
## For a message- or recipient-oriented routine, accept this message, but
|
|
||||||
# silently discard it. SMFIS_DISCARD should not be returned by a
|
|
||||||
# connection-oriented routine.
|
|
||||||
DISCARD = 2
|
|
||||||
|
|
||||||
## For a connection-oriented routine, accept this connection without further
|
|
||||||
# filter processing; call Milter.Base.close(). For a message- or
|
|
||||||
# recipient-oriented routine, accept this message without further filtering.
|
|
||||||
ACCEPT = 3
|
|
||||||
|
|
||||||
## Return a temporary failure, i.e., the corresponding SMTP command will return
|
|
||||||
# an appropriate 4xx status code. For a message-oriented routine, except
|
|
||||||
# Milter.Base.envfrom(), fail for this message. For a connection-oriented
|
|
||||||
# routine, fail for this connection; call Milter.Base.close(). For a recipient-oriented
|
|
||||||
# routine, only
|
|
||||||
# fail for the current recipient; continue message processing.
|
|
||||||
TEMPFAIL = 4
|
|
||||||
|
|
||||||
## Skip further callbacks of the same type in this transaction.
|
|
||||||
# Currently this return value is only allowed in Milter.Base.body(). It can be
|
|
||||||
# used if a %milter has received sufficiently many body chunks to make a
|
|
||||||
# decision, but still wants to invoke message modification functions that are
|
|
||||||
# only allowed to be called from Milter.Base.eom(). Note: the %milter must
|
|
||||||
# negotiate this behavior with the MTA, i.e., it must check whether the
|
|
||||||
# protocol action SMFIP_SKIP is available and if so, the %milter must request
|
|
||||||
# it.
|
|
||||||
SKIP = 5
|
|
||||||
|
|
||||||
## Do not send a reply back to the MTA.
|
|
||||||
# The %milter must negotiate this behavior with the MTA, i.e., it must check
|
|
||||||
# whether the appropriate protocol action P_NR_* is available and if so,
|
|
||||||
# the %milter must request it. If you set the P_NR_* protocol action for a
|
|
||||||
# callback, that callback must always reply with NOREPLY. Using any other
|
|
||||||
# reply code is a violation of the API. If in some cases your callback may
|
|
||||||
# return another value (e.g., due to some resource shortages), then you must
|
|
||||||
# not set P_NR_* and you must use CONTINUE as the default return
|
|
||||||
# code. (Alternatively you can try to delay reporting the problem to a later
|
|
||||||
# callback for which P_NR_* is not set.)
|
|
||||||
#
|
|
||||||
# This is negotiated and returned automatically by the Milter.noreply
|
|
||||||
# function decorator.
|
|
||||||
NOREPLY = 6
|
|
||||||
|
|
||||||
## Hold context for a %milter connection.
|
|
||||||
# Each connection to sendmail creates a new <code>SMFICTX</code> struct within
|
|
||||||
# libmilter. The milter module in turn creates a milterContext
|
|
||||||
# tied to the <code>SMFICTX</code> struct via <code>smfi_setpriv</code>
|
|
||||||
# to hold a PyThreadState and a user defined Python object for the connection.
|
|
||||||
#
|
|
||||||
# Most application interaction with libmilter takes places via
|
|
||||||
# the milterContext object for the connection. It is passed to
|
|
||||||
# callback functions as the first parameter.
|
|
||||||
#
|
|
||||||
# The <code>Milter</code> module creates a python class for each connection,
|
|
||||||
# and converts function callbacks to instance method invocations.
|
|
||||||
#
|
|
||||||
class milterContext(object):
|
|
||||||
## Calls <a href="milter_api/smfi_getsymval.html">smfi_getsymval</a>.
|
|
||||||
def getsymval(self,sym): pass
|
|
||||||
## Calls <a href="milter_api/smfi_setreply.html">
|
|
||||||
# smfi_setreply</a> or
|
|
||||||
# <a href="milter_api/smfi_setmlreply.html">
|
|
||||||
# smfi_setmlreply</a>.
|
|
||||||
# @param rcode SMTP response code
|
|
||||||
# @param xcode extended SMTP response code
|
|
||||||
# @param msg one or more message lines. If the MTA does not support
|
|
||||||
# multiline messages, only the first is used.
|
|
||||||
def setreply(self,rcode,xcode,*msg): pass
|
|
||||||
## Calls <a href="milter_api/smfi_addheader.html">smfi_addheader</a>.
|
|
||||||
def addheader(self,name,value,idx=-1): pass
|
|
||||||
## Calls <a href="milter_api/smfi_chgheader.html">smfi_chgheader</a>.
|
|
||||||
def chgheader(self,name,idx,value): pass
|
|
||||||
## Calls <a href="milter_api/smfi_addrcpt.html">smfi_addrcpt</a>.
|
|
||||||
def addrcpt(self,rcpt,params=None): pass
|
|
||||||
## Calls <a href="milter_api/smfi_delrcpt.html">smfi_delrcpt</a>.
|
|
||||||
def delrcpt(self,rcpt): pass
|
|
||||||
## Calls <a href="milter_api/smfi_replacebody.html">smfi_replacebody</a>.
|
|
||||||
def replacebody(self,data): pass
|
|
||||||
## Attach a Python object to this connection context.
|
|
||||||
# @return the old value or None
|
|
||||||
def setpriv(self,priv): pass
|
|
||||||
## Return the Python object attached to this connection context.
|
|
||||||
def getpriv(self): pass
|
|
||||||
## Calls <a href="milter_api/smfi_quarantine.html">smfi_quarantine</a>.
|
|
||||||
def quarantine(self,reason): pass
|
|
||||||
## Calls <a href="milter_api/smfi_progress.html">smfi_progress</a>.
|
|
||||||
def progress(self): pass
|
|
||||||
## Calls <a href="milter_api/smfi_chgfrom.html">smfi_chgfrom</a>.
|
|
||||||
def chgfrom(self,sender,param=None): pass
|
|
||||||
## Tell the MTA which macro values we are interested in for a given stage.
|
|
||||||
# Of interest only when you need to squeeze a few more bytes of bandwidth.
|
|
||||||
# It may only be called from the negotiate callback.
|
|
||||||
# The protocol stages are
|
|
||||||
# M_CONNECT, M_HELO, M_ENVFROM, M_ENVRCPT, M_DATA, M_EOM, M_EOH.
|
|
||||||
# Calls <a href="milter_api/smfi_setsymlist.html">smfi_setsymlist</a>.
|
|
||||||
# @param stage protocol stage in which the macro list should be used
|
|
||||||
# @param macrolist a space separated list of macro names
|
|
||||||
def setsymlist(self,stage,macrolist): pass
|
|
||||||
|
|
||||||
class error(Exception): pass
|
|
||||||
|
|
||||||
## Enable optional %milter actions.
|
|
||||||
# Certain %milter actions need to be enabled before calling main()
|
|
||||||
# or they throw an exception. Pymilter enables them all by
|
|
||||||
# default (since 0.9.2), but you may wish to disable unneeded
|
|
||||||
# actions as an optimization.
|
|
||||||
# @param flags Bit or mask of optional actions to enable
|
|
||||||
def set_flags(flags): pass
|
|
||||||
|
|
||||||
def set_connect_callback(cb): pass
|
|
||||||
def set_helo_callback(cb): pass
|
|
||||||
def set_envfrom_callback(cb): pass
|
|
||||||
def set_envrcpt_callback(cb): pass
|
|
||||||
def set_header_callback(cb): pass
|
|
||||||
def set_eoh_callback(cb): pass
|
|
||||||
def set_body_callback(cb): pass
|
|
||||||
def set_abort_callback(cb): pass
|
|
||||||
def set_close_callback(cb): pass
|
|
||||||
|
|
||||||
## Sets the return code for untrapped Python exceptions during a callback.
|
|
||||||
# The default is TEMPFAIL. You should not depend on this handler. Your
|
|
||||||
# application should have its own top level exception handler for each
|
|
||||||
# callback. You can then choose your own reply message, log the stack track
|
|
||||||
# were you please, and so on. However, if you miss one, this last ditch
|
|
||||||
# handler will print a standard stack trace to sys.stderr, and return to
|
|
||||||
# sendmail.
|
|
||||||
# @param code one of #TEMPFAIL,#REJECT,#CONTINUE, or since 1.0, #ACCEPT
|
|
||||||
def set_exception_policy(code): pass
|
|
||||||
|
|
||||||
## Register python %milter with libmilter.
|
|
||||||
# The name we pass is used to identify the %milter in the MTA configuration.
|
|
||||||
# Callback functions must be set using the set_*_callback() functions before
|
|
||||||
# registering the %milter.
|
|
||||||
# Three additional callbacks are specified as keyword parameters. These
|
|
||||||
# were added by recent versions of libmilter. The keyword parameters is
|
|
||||||
# a nicer way to do it, I think, since it makes clear that you have to do
|
|
||||||
# it before registering. I may move all the callbacks in the future (perhaps
|
|
||||||
# keeping the set functions for compatibility). Note that Milter.Base
|
|
||||||
# automatically maps all callbacks to member functions, and negotiates which
|
|
||||||
# member functions are actually overridden by an application class.
|
|
||||||
# @param name the %milter name by which the MTA finds us
|
|
||||||
# @param negotiate the
|
|
||||||
# <a href="milter_api/xxfi_negotiate.html">
|
|
||||||
# xxfi_negotiate</a> callback, called to negotiate supported
|
|
||||||
# actions, callbacks, and protocol steps.
|
|
||||||
# @param unknown the
|
|
||||||
# <a href="milter_api/xxfi_unknown.html">
|
|
||||||
# xxfi_unknown</a> callback, called when for SMTP commands
|
|
||||||
# not recognized by the MTA. (Extend SMTP in your milter!)
|
|
||||||
# @param data the
|
|
||||||
# <a href="milter_api/xxfi_data.html">
|
|
||||||
# xxfi_data</a> callback, called when the DATA
|
|
||||||
# SMTP command is received.
|
|
||||||
def register(name,negotiate=None,unknown=None,data=None): pass
|
|
||||||
|
|
||||||
## Attempt to create the socket used to communicate with the MTA.
|
|
||||||
# milter.opensocket() attempts to create the socket specified previously by a
|
|
||||||
# call to milter.setconn() which will be the interface between MTAs and the
|
|
||||||
# %milter. This allows the calling application to ensure that the socket can be
|
|
||||||
# created. If this is not called, milter.main() will do so implicitly.
|
|
||||||
# Calls <a href="milter_api/smfi_opensocket.html">
|
|
||||||
# smfi_opensocket</a>. While not documented for libmilter, my experiments
|
|
||||||
# indicate that you must call register() before calling opensocket().
|
|
||||||
# @param rmsock Try to remove an existing unix domain socket if true.
|
|
||||||
def opensocket(rmsock): pass
|
|
||||||
|
|
||||||
## Transfer control to libmilter.
|
|
||||||
# Calls <a href="milter_api/smfi_main.html">
|
|
||||||
# smfi_main</a>.
|
|
||||||
def main(): pass
|
|
||||||
|
|
||||||
## Set the libmilter debugging level.
|
|
||||||
# <a href="milter_api/smfi_setdbg.html">smfi_setdbg</a>
|
|
||||||
# sets the %milter library's internal debugging level to a new level
|
|
||||||
# so that code details may be traced. A level of zero turns off debugging. The
|
|
||||||
# greater (more positive) the level the more detailed the debugging. Six is the
|
|
||||||
# current, highest, useful value. Must be called before calling main().
|
|
||||||
def setdbg(lev): pass
|
|
||||||
|
|
||||||
## Set timeout for MTA communication.
|
|
||||||
# Calls <a href="milter_api/smfi_settimeout.html">
|
|
||||||
# smfi_settimeout</a>. Must be called before calling main().
|
|
||||||
def settimeout(secs): pass
|
|
||||||
|
|
||||||
## Set socket backlog.
|
|
||||||
# Calls <a href="milter_api/smfi_setbacklog.html">
|
|
||||||
# smfi_setbacklog</a>. Must be called before calling main().
|
|
||||||
def setbacklog(n): pass
|
|
||||||
|
|
||||||
## Set the socket used to communicate with the MTA.
|
|
||||||
# The MTA can communicate with the milter by means of a
|
|
||||||
# unix, inet, or inet6 socket. By default, a unix domain socket
|
|
||||||
# is used. It must not exist,
|
|
||||||
# and sendmail will throw warnings if, eg, the file is under a
|
|
||||||
# group or world writable directory. milter.setconn() will not fail with
|
|
||||||
# an invalid socket - this will be detected only when calling milter.main()
|
|
||||||
# or milter.opensocket().
|
|
||||||
# @param s the socket address in proto:address format
|
|
||||||
# <pre>
|
|
||||||
# milter.setconn('unix:/var/run/pythonfilter') # a named pipe
|
|
||||||
# milter.setconn('local:/var/run/pythonfilter') # a named pipe
|
|
||||||
# milter.setconn('inet:8800') # listen on ANY interface
|
|
||||||
# milter.setconn('inet:7871@@publichost') # listen on a specific interface
|
|
||||||
# milter.setconn('inet6:8020')
|
|
||||||
# milter.setconn('inet6:8020@[2001:db8:1234::1]') # listen on specific IP
|
|
||||||
# </pre>
|
|
||||||
def setconn(s): pass
|
|
||||||
|
|
||||||
## Stop the %milter gracefully.
|
|
||||||
def stop(): pass
|
|
||||||
|
|
||||||
## Retrieve diagnostic info.
|
|
||||||
# Return a tuple with diagnostic info gathered by the milter module.
|
|
||||||
# The first two fields are counts of milterContext objects created
|
|
||||||
# and deleted. Additional fields may be added later.
|
|
||||||
# @return a tuple of diagnostic data
|
|
||||||
def getdiag(): pass
|
|
||||||
|
|
||||||
## Retrieve the runtime libmilter version.
|
|
||||||
# Return the runtime libmilter version. This can be different
|
|
||||||
# from the compile time version when sendmail or libmilter is upgraded
|
|
||||||
# after pymilter is compiled.
|
|
||||||
# @return a tuple of <code>(major,minor,patchlevel)</code>
|
|
||||||
def getversion(): pass
|
|
||||||
|
|
||||||
## The compile time libmilter version.
|
|
||||||
# Python code might need to deal with pymilter compiled
|
|
||||||
# against various versions of libmilter. This module constant
|
|
||||||
# contains the contents of the <code>SMFI_VERSION</code> macro when
|
|
||||||
# the milter module was compiled.
|
|
||||||
VERSION = 0x1000001
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
web:
|
|
||||||
doxygen
|
|
||||||
test -L doc/html/milter_api || ln -sf /usr/share/doc/sendmail-milter-devel doc/html/milter_api
|
|
||||||
rsync -ravKk doc/html/ pymilter.org:/var/www/html/milter/pymilter
|
|
||||||
cd doc/html; zip -r ../../doc .
|
|
||||||
|
|
||||||
VERSION=1.0.5
|
|
||||||
PKG=pymilter-$(VERSION)
|
|
||||||
SRCTAR=$(PKG).tar.gz
|
|
||||||
|
|
||||||
$(SRCTAR):
|
|
||||||
git archive --format=tar.gz --prefix=$(PKG)/ -o $(SRCTAR) $(PKG)
|
|
||||||
|
|
||||||
# add extra copy of name like github so annoyingly does...
|
|
||||||
github:
|
|
||||||
git archive --format=tar.gz --prefix=pymilter-$(PKG)/ -o $(SRCTAR) $(PKG)
|
|
||||||
|
|
||||||
gittar: $(SRCTAR)
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
## A very simple sample milter to prevent mixing of internal and external mail.
|
|
||||||
# Internal is defined as using one of a list of internal top level domains.
|
|
||||||
# This code is open-source on the same terms as Python.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
import Milter
|
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
from Milter.utils import parse_addr
|
|
||||||
|
|
||||||
internal_tlds = ["corp", "personal"]
|
|
||||||
|
|
||||||
## Determine if a hostname is internal or not.
|
|
||||||
# True if internal, False otherwise
|
|
||||||
def is_internal(hostname):
|
|
||||||
components = hostname.split(".")
|
|
||||||
return components.pop() in internal_tlds
|
|
||||||
|
|
||||||
# Determine if internal and external hosts are mixed based on a list
|
|
||||||
# of hostnames
|
|
||||||
def are_mixed(hostnames):
|
|
||||||
hostnames_mapped = map(is_internal, hostnames)
|
|
||||||
|
|
||||||
# Num internals
|
|
||||||
num_internal_hosts = hostnames_mapped.count(True)
|
|
||||||
|
|
||||||
# Num externals
|
|
||||||
num_external_hosts = hostnames_mapped.count(False)
|
|
||||||
|
|
||||||
return num_external_hosts >= 1 and num_internal_hosts >= 1
|
|
||||||
|
|
||||||
class NoMixMilter(Milter.Base):
|
|
||||||
|
|
||||||
def __init__(self): # A new instance with each new connection.
|
|
||||||
self.id = Milter.uniqueID() # Integer incremented with each call.
|
|
||||||
|
|
||||||
|
|
||||||
## def envfrom(self,f,*str):
|
|
||||||
@Milter.noreply
|
|
||||||
def envfrom(self, mailfrom, *str):
|
|
||||||
self.mailfrom = mailfrom
|
|
||||||
self.domains = []
|
|
||||||
t = parse_addr(mailfrom)
|
|
||||||
if len(t) > 1:
|
|
||||||
self.domains.append(t[1])
|
|
||||||
else:
|
|
||||||
self.domains.append('local')
|
|
||||||
self.internal = False
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
## def envrcpt(self, to, *str):
|
|
||||||
def envrcpt(self, to, *str):
|
|
||||||
self.R.append(to)
|
|
||||||
t = parse_addr(to)
|
|
||||||
if len(t) > 1:
|
|
||||||
self.domains.append(t[1])
|
|
||||||
else:
|
|
||||||
self.domains.append('local')
|
|
||||||
|
|
||||||
if are_mixed(self.domains):
|
|
||||||
# FIXME: log recipients collected in self.mailfrom and self.R
|
|
||||||
self.setreply('550','5.7.1','Mixing internal and external TLDs')
|
|
||||||
return Milter.REJECT
|
|
||||||
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def main():
|
|
||||||
socketname = "/var/run/nomixsock"
|
|
||||||
timeout = 600
|
|
||||||
# Register to have the Milter factory create instances of your class:
|
|
||||||
Milter.factory = NoMixMilter
|
|
||||||
print("%s milter startup" % time.strftime('%Y%b%d %H:%M:%S'))
|
|
||||||
sys.stdout.flush()
|
|
||||||
Milter.runmilter("nomixfilter",socketname,timeout)
|
|
||||||
logq.put(None)
|
|
||||||
bt.join()
|
|
||||||
print("%s nomix milter shutdown" % time.strftime('%Y%b%d %H:%M:%S'))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+253
@@ -0,0 +1,253 @@
|
|||||||
|
%define name milter
|
||||||
|
%define version 0.8.0
|
||||||
|
%define release 3.RH7
|
||||||
|
# what version of RH are we building for?
|
||||||
|
%define redhat9 0
|
||||||
|
%define redhat7 1
|
||||||
|
%define redhat6 0
|
||||||
|
|
||||||
|
# Options for Redhat version 6.x:
|
||||||
|
# rpm -ba|--rebuild --define "rh6 1"
|
||||||
|
%{?rh6:%define redhat7 0}
|
||||||
|
%{?rh6:%define redhat6 1}
|
||||||
|
|
||||||
|
# some systems dont have initrddir defined
|
||||||
|
%{?_initrddir:%define _initrddir /etc/rc.d/init.d}
|
||||||
|
|
||||||
|
%if %{redhat9}
|
||||||
|
%define sysvinit milter.rc
|
||||||
|
%else # Redhat 7.x and earlier (multiple ps lines per thread)
|
||||||
|
%define sysvinit milter.rc7
|
||||||
|
%endif
|
||||||
|
# RH9, other systems (single ps line per process)
|
||||||
|
%ifos Linux
|
||||||
|
%define python python2.4
|
||||||
|
%else
|
||||||
|
%define python python
|
||||||
|
%endif
|
||||||
|
|
||||||
|
Summary: Python interface to sendmail milter API
|
||||||
|
Name: %{name}
|
||||||
|
Version: %{version}
|
||||||
|
Release: %{release}
|
||||||
|
Source: %{name}-%{version}.tar.gz
|
||||||
|
#Patch: %{name}-%{version}.patch
|
||||||
|
Copyright: GPL
|
||||||
|
Group: Development/Libraries
|
||||||
|
BuildRoot: %{_tmppath}/%{name}-buildroot
|
||||||
|
Prefix: %{_prefix}
|
||||||
|
Vendor: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
Packager: Stuart D. Gathman <stuart@bmsi.com>
|
||||||
|
Url: http://www.bmsi.com/python/milter.html
|
||||||
|
Requires: %{python} >= 2.4, sendmail >= 8.12.10
|
||||||
|
%ifos Linux
|
||||||
|
Requires: chkconfig
|
||||||
|
%endif
|
||||||
|
BuildRequires: %{python}-devel , sendmail-devel >= 8.12.10
|
||||||
|
|
||||||
|
%description
|
||||||
|
This is a python extension module to enable python scripts to
|
||||||
|
attach to sendmail's libmilter functionality. Additional python
|
||||||
|
modules provide for navigating and modifying MIME parts.
|
||||||
|
|
||||||
|
%prep
|
||||||
|
%setup
|
||||||
|
#%patch -p1
|
||||||
|
|
||||||
|
%build
|
||||||
|
env CFLAGS="$RPM_OPT_FLAGS" %{python} setup.py build
|
||||||
|
|
||||||
|
%install
|
||||||
|
rm -rf $RPM_BUILD_ROOT
|
||||||
|
%{python} setup.py install --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/var/log/milter
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/etc/mail
|
||||||
|
mkdir $RPM_BUILD_ROOT/var/log/milter/save
|
||||||
|
cp bms.py strike3.txt softfail.txt $RPM_BUILD_ROOT/var/log/milter
|
||||||
|
cp milter.cfg $RPM_BUILD_ROOT/etc/mail/pymilter.cfg
|
||||||
|
|
||||||
|
# logfile rotation
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/etc/logrotate.d
|
||||||
|
cat >$RPM_BUILD_ROOT/etc/logrotate.d/milter <<'EOF'
|
||||||
|
/var/log/milter/milter.log {
|
||||||
|
copytruncate
|
||||||
|
compress
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# purge saved defanged message copies
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/etc/cron.daily
|
||||||
|
%ifos aix4.1
|
||||||
|
R=
|
||||||
|
%else
|
||||||
|
R='-r'
|
||||||
|
%endif
|
||||||
|
cat >$RPM_BUILD_ROOT/etc/cron.daily/milter <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
find /var/log/milter/save -mtime +7 | xargs $R rm
|
||||||
|
EOF
|
||||||
|
chmod a+x $RPM_BUILD_ROOT/etc/cron.daily/milter
|
||||||
|
|
||||||
|
%ifos aix4.1
|
||||||
|
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
cd /var/log/milter
|
||||||
|
# uncomment to enable sgmlop if installed
|
||||||
|
#export PYTHONPATH=/usr/local/lib/python2.1/site-packages
|
||||||
|
exec /usr/local/bin/python bms.py >>milter.log 2>&1
|
||||||
|
EOF
|
||||||
|
%else
|
||||||
|
cat >$RPM_BUILD_ROOT/var/log/milter/start.sh <<'EOF'
|
||||||
|
#!/bin/sh
|
||||||
|
cd /var/log/milter
|
||||||
|
exec >>milter.log 2>&1
|
||||||
|
%{python} bms.py &
|
||||||
|
echo $! >/var/run/milter/milter.pid
|
||||||
|
EOF
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d
|
||||||
|
cp %{sysvinit} $RPM_BUILD_ROOT/etc/rc.d/init.d/milter
|
||||||
|
ed $RPM_BUILD_ROOT/etc/rc.d/init.d/milter <<'EOF'
|
||||||
|
/^python=/
|
||||||
|
c
|
||||||
|
python="%{python}"
|
||||||
|
.
|
||||||
|
w
|
||||||
|
q
|
||||||
|
EOF
|
||||||
|
%endif
|
||||||
|
chmod a+x $RPM_BUILD_ROOT/var/log/milter/start.sh
|
||||||
|
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/var/run/milter
|
||||||
|
mkdir -p $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
|
||||||
|
cp -p rhsbl.m4 $RPM_BUILD_ROOT/usr/share/sendmail-cf/hack
|
||||||
|
|
||||||
|
%ifos aix4.1
|
||||||
|
%post
|
||||||
|
mkssys -s milter -p /var/log/milter/start.sh -u 25 -S -n 15 -f 9 -G mail || :
|
||||||
|
|
||||||
|
%preun
|
||||||
|
if [ $1 = 0 ]; then
|
||||||
|
rmssys -s milter || :
|
||||||
|
fi
|
||||||
|
%else
|
||||||
|
%post
|
||||||
|
#echo "pythonsock has moved to /var/run/milter, update /etc/mail/sendmail.cf"
|
||||||
|
/sbin/chkconfig --add milter
|
||||||
|
|
||||||
|
%preun
|
||||||
|
if [ $1 = 0 ]; then
|
||||||
|
/sbin/chkconfig --del milter
|
||||||
|
fi
|
||||||
|
%endif
|
||||||
|
|
||||||
|
%clean
|
||||||
|
rm -rf $RPM_BUILD_ROOT
|
||||||
|
|
||||||
|
%files -f INSTALLED_FILES
|
||||||
|
%defattr(-,root,root)
|
||||||
|
%doc README NEWS TODO CREDITS sample.py
|
||||||
|
/etc/logrotate.d/milter
|
||||||
|
/etc/cron.daily/milter
|
||||||
|
%ifos aix4.1
|
||||||
|
%defattr(-,smmsp,mail)
|
||||||
|
%else
|
||||||
|
/etc/rc.d/init.d/milter
|
||||||
|
%defattr(-,mail,mail)
|
||||||
|
%endif
|
||||||
|
%dir /var/log/milter
|
||||||
|
%dir /var/run/milter
|
||||||
|
%dir /var/log/milter/save
|
||||||
|
%config /var/log/milter/start.sh
|
||||||
|
%config /var/log/milter/bms.py
|
||||||
|
%config /var/log/milter/strike3.txt
|
||||||
|
%config /var/log/milter/softfail.txt
|
||||||
|
%config(noreplace) /etc/mail/pymilter.cfg
|
||||||
|
/usr/share/sendmail-cf/hack/rhsbl.m4
|
||||||
|
|
||||||
|
%changelog
|
||||||
|
* Mon Jun 06 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-3
|
||||||
|
- properly log pydspam exceptions
|
||||||
|
* Sat Jun 04 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-2
|
||||||
|
- Include default softfail, strike3 templates
|
||||||
|
* Wed May 25 2005 Stuart Gathman <stuart@bmsi.com> 0.8.0-1
|
||||||
|
- Move Milter module to subpackage.
|
||||||
|
- DSN support for Three strikes rule and SPF SOFTFAIL
|
||||||
|
- Move /*mime*/ and dynip to Milter subpackage
|
||||||
|
- Fix SPF unknown mechanism list not cleared
|
||||||
|
- Make banned extensions configurable.
|
||||||
|
- Option to scan zipfiles for bad extensions.
|
||||||
|
* Tue Feb 08 2005 Stuart Gathman <stuart@bmsi.com> 0.7.3-1.EL3
|
||||||
|
- Support EL3 and Python2.4 (some scanning/defang support broken)
|
||||||
|
* Mon Aug 30 2004 Stuart Gathman <stuart@bmsi.com> 0.7.2-1
|
||||||
|
- Fix various SPF bugs
|
||||||
|
- Recognize dynamic PTR names, and don't count them as authentication.
|
||||||
|
- Three strikes and yer out rule.
|
||||||
|
- Block softfail by default unless valid PTR or HELO
|
||||||
|
- Return unknown for null mechanism
|
||||||
|
- Return unknown for invalid ip address in mechanism
|
||||||
|
- Try best guess on HELO also
|
||||||
|
- Expand setreply for common errors
|
||||||
|
- make rhsbl.m4 hack available for sendmail.mc
|
||||||
|
* Sun Aug 22 2004 Stuart Gathman <stuart@bmsi.com> 0.7.1-1
|
||||||
|
- Handle modifying mislabeled multipart messages without an exception
|
||||||
|
- Support setbacklog, setmlreply
|
||||||
|
- allow multi-recipient CBV
|
||||||
|
- return TEMPFAIL for SPF softfail
|
||||||
|
* Fri Jul 23 2004 Stuart Gathman <stuart@bmsi.com> 0.7.0-1
|
||||||
|
- SPF check hello name
|
||||||
|
- Move pythonsock to /var/run/milter
|
||||||
|
- Move milter.cfg to /etc/mail/pymilter.cfg
|
||||||
|
- Check M$ style XML CID records by converting to SPF
|
||||||
|
- Recognize, but never match ip6 until we properly support it.
|
||||||
|
- Option to reject when no PTR and no SPF
|
||||||
|
* Fri Apr 09 2004 Stuart Gathman <stuart@bmsi.com> 0.6.9-1
|
||||||
|
- Validate spf.py against test suite, and add Received-SPF support to spf.py
|
||||||
|
- Support best_guess for SPF
|
||||||
|
- Reject numeric hello names
|
||||||
|
- Preserve case of local part in sender
|
||||||
|
- Make libmilter timeout a config option
|
||||||
|
- Fix setup.py to work with python < 2.2.3
|
||||||
|
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-3
|
||||||
|
- Reject invalid SRS immediately for benefit of callback verifiers
|
||||||
|
- Fix include bug in spf.py
|
||||||
|
* Tue Apr 06 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-2
|
||||||
|
- Bug in check_header
|
||||||
|
* Mon Apr 05 2004 Stuart Gathman <stuart@bmsi.com> 0.6.8-1
|
||||||
|
- Don't report spoofed unless rcpt looks like SRS
|
||||||
|
- Check for bounce with multiple rcpts
|
||||||
|
- Make dspam see Received-SPF headers
|
||||||
|
- Make sysv init work with RH9
|
||||||
|
* Thu Mar 25 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-3
|
||||||
|
- Forgot to make spf_reject_neutral global in bms.py
|
||||||
|
* Wed Mar 24 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-2
|
||||||
|
- Defang message/rfc822 content_type with boundary
|
||||||
|
- Support SPF delegation
|
||||||
|
- Reject neutral SPF result for selected domains
|
||||||
|
* Tue Mar 23 2004 Stuart Gathman <stuart@bmsi.com> 0.6.7-1
|
||||||
|
- SRS forgery check. Detect thread resource starvation.
|
||||||
|
- Properly remove local socket with explicit type.
|
||||||
|
- Decode obfuscated subject headers.
|
||||||
|
* Wed Mar 11 2004 Stuart Gathman <stuart@bmsi.com> 0.6.6-2
|
||||||
|
- init script bug with python2.3
|
||||||
|
* Wed Mar 10 2004 Stuart Gathman <stuart@bmsi.com> 0.6.6-1
|
||||||
|
- SPF checking, hello blacklist
|
||||||
|
* Mon Mar 08 2004 Stuart Gathman <stuart@bmsi.com> 0.6.5-2
|
||||||
|
- memory leak in envfrom and envrcpt
|
||||||
|
* Mon Mar 01 2004 Stuart Gathman <stuart@bmsi.com> 0.6.5-1
|
||||||
|
- progress notification
|
||||||
|
- memory leak in connect
|
||||||
|
- trusted relay
|
||||||
|
* Thu Feb 19 2004 Stuart Gathman <stuart@bmsi.com> 0.6.4-2
|
||||||
|
- smart alias wildcard patch, compile for sendmail-8.12
|
||||||
|
* Thu Dec 04 2003 Stuart Gathman <stuart@bmsi.com> 0.6.4-1
|
||||||
|
- many fixes for dspam support
|
||||||
|
* Wed Oct 22 2003 Stuart Gathman <stuart@bmsi.com> 0.6.3
|
||||||
|
- dspam SCREEN feature
|
||||||
|
- streamline dspam false positive handling
|
||||||
|
* Mon Sep 01 2003 Stuart Gathman <stuart@bmsi.com> 0.6.1
|
||||||
|
- Full dspam support added
|
||||||
|
* Mon Aug 26 2003 Stuart Gathman <stuart@bmsi.com>
|
||||||
|
- Use New email module
|
||||||
|
* Fri Jun 27 2003 Stuart Gathman <stuart@bmsi.com>
|
||||||
|
- Add dspam module
|
||||||
-1589
File diff suppressed because it is too large
Load Diff
@@ -1,487 +0,0 @@
|
|||||||
## @package mime
|
|
||||||
# This module provides a "defang" function to replace naughty attachments.
|
|
||||||
#
|
|
||||||
# We also provide workarounds for bugs in the email module that comes
|
|
||||||
# with python. The "bugs" fixed mostly come up only with malformed
|
|
||||||
# messages - but that is what you have when dealing with spam.
|
|
||||||
|
|
||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2001,2002,2003,2004,2005 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
try:
|
|
||||||
from io import BytesIO, StringIO
|
|
||||||
except:
|
|
||||||
from StringIO import StringIO
|
|
||||||
BytesIO = StringIO
|
|
||||||
import socket
|
|
||||||
import Milter
|
|
||||||
import zipfile
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import email
|
|
||||||
from email.message import Message
|
|
||||||
try:
|
|
||||||
from email.generator import BytesGenerator
|
|
||||||
from email import message_from_binary_file, encoders
|
|
||||||
except:
|
|
||||||
from email.generator import Generator as BytesGenerator
|
|
||||||
from email import message_from_file as message_from_binary_file
|
|
||||||
from email import Encoders as encoders
|
|
||||||
from email.utils import quote
|
|
||||||
|
|
||||||
if not getattr(Message,'as_bytes',None):
|
|
||||||
Message.as_bytes = Message.as_string
|
|
||||||
|
|
||||||
## Return a list of filenames in a zip file.
|
|
||||||
# Embedded zip files are recursively expanded.
|
|
||||||
def zipnames(txt):
|
|
||||||
fp = BytesIO(txt)
|
|
||||||
zipf = zipfile.ZipFile(fp,'r')
|
|
||||||
names = []
|
|
||||||
for nm in zipf.namelist():
|
|
||||||
names.append(('zipname',nm))
|
|
||||||
if nm.lower().endswith('.zip'):
|
|
||||||
names += zipnames(zipf.read(nm))
|
|
||||||
return names
|
|
||||||
|
|
||||||
## Fix multipart handling in email.Generator.
|
|
||||||
#
|
|
||||||
class MimeGenerator(BytesGenerator):
|
|
||||||
def _dispatch(self, msg):
|
|
||||||
# Get the Content-Type: for the message, then try to dispatch to
|
|
||||||
# self._handle_<maintype>_<subtype>(). If there's no handler for the
|
|
||||||
# full MIME type, then dispatch to self._handle_<maintype>(). If
|
|
||||||
# that's missing too, then dispatch to self._writeBody().
|
|
||||||
main = msg.get_content_maintype()
|
|
||||||
if msg.is_multipart() and main.lower() != 'multipart':
|
|
||||||
self._handle_multipart(msg)
|
|
||||||
else:
|
|
||||||
BytesGenerator._dispatch(self,msg)
|
|
||||||
|
|
||||||
def unquote(s):
|
|
||||||
"""Remove quotes from a string."""
|
|
||||||
if len(s) > 1:
|
|
||||||
if s.startswith('"'):
|
|
||||||
if s.endswith('"'):
|
|
||||||
s = s[1:-1]
|
|
||||||
else: # remove garbage after trailing quote
|
|
||||||
try: s = s[1:s[1:].index('"')+1]
|
|
||||||
except:
|
|
||||||
return s
|
|
||||||
return s.replace('\\\\', '\\').replace('\\"', '"')
|
|
||||||
if s.startswith('<') and s.endswith('>'):
|
|
||||||
return s[1:-1]
|
|
||||||
return s
|
|
||||||
|
|
||||||
def _unquotevalue(value):
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
return value[0], value[1], unquote(value[2])
|
|
||||||
else:
|
|
||||||
return unquote(value)
|
|
||||||
|
|
||||||
#email.Message._unquotevalue = _unquotevalue
|
|
||||||
|
|
||||||
from email.message import _parseparam
|
|
||||||
|
|
||||||
## Enhance email.message.Message
|
|
||||||
#
|
|
||||||
# Tracks modifications to headers of body or any part independently.
|
|
||||||
|
|
||||||
class MimeMessage(Message):
|
|
||||||
"""Version of email.Message.Message compatible with old mime module
|
|
||||||
"""
|
|
||||||
def __init__(self,fp=None,seekable=1):
|
|
||||||
Message.__init__(self)
|
|
||||||
self.submsg = None
|
|
||||||
self.modified = False
|
|
||||||
## @var headerchange
|
|
||||||
# Provide a headerchange event for integration with Milter.
|
|
||||||
# The headerchange attribute can be assigned a function to be called when
|
|
||||||
# changing headers. The signature is:
|
|
||||||
# headerchange(msg,name,value) -> None
|
|
||||||
self.headerchange = None
|
|
||||||
|
|
||||||
def get_param(self, param, failobj=None, header='content-type', unquote=True):
|
|
||||||
val = Message.get_param(self,param,failobj,header,unquote)
|
|
||||||
if val != failobj and param == 'boundary' and unquote:
|
|
||||||
# unquote boundaries an extra time, test case testDefang5
|
|
||||||
return _unquotevalue(val)
|
|
||||||
return val
|
|
||||||
|
|
||||||
getfilename = Message.get_filename
|
|
||||||
ismultipart = Message.is_multipart
|
|
||||||
getheaders = Message.get_all
|
|
||||||
gettype = Message.get_content_type
|
|
||||||
getparam = Message.get_param
|
|
||||||
|
|
||||||
def getparams(self): return self.get_params([])
|
|
||||||
|
|
||||||
def getname(self):
|
|
||||||
return self.get_param('name')
|
|
||||||
|
|
||||||
def getnames(self,scan_zip=False):
|
|
||||||
"""Return a list of (attr,name) pairs of attributes that IE might
|
|
||||||
interpret as a name - and hence decide to execute this message."""
|
|
||||||
names = []
|
|
||||||
for attr,val in self.get_params([],'content-type',False):
|
|
||||||
if isinstance(val, tuple):
|
|
||||||
# It's an RFC 2231 encoded parameter
|
|
||||||
newvalue = _unquotevalue(val)
|
|
||||||
if val[0]:
|
|
||||||
val = unicode(newvalue[2], newvalue[0])
|
|
||||||
else:
|
|
||||||
val = unicode(newvalue[2])
|
|
||||||
else:
|
|
||||||
val = _unquotevalue(val.strip())
|
|
||||||
names.append((attr,val))
|
|
||||||
names += [("filename",self.get_filename())]
|
|
||||||
if scan_zip:
|
|
||||||
for key,name in tuple(names): # copy by converting to tuple
|
|
||||||
if name and name.lower().endswith('.zip'):
|
|
||||||
txt = self.get_payload(decode=True)
|
|
||||||
if txt.strip():
|
|
||||||
names += zipnames(txt)
|
|
||||||
return names
|
|
||||||
|
|
||||||
def ismodified(self):
|
|
||||||
"True if this message or a subpart has been modified."
|
|
||||||
if not self.is_multipart():
|
|
||||||
if isinstance(self.submsg,Message):
|
|
||||||
return self.submsg.ismodified()
|
|
||||||
return self.modified
|
|
||||||
if self.modified: return True
|
|
||||||
for i in self.get_payload():
|
|
||||||
if i.ismodified(): return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def dump(self,file,unixfrom=False):
|
|
||||||
"Write this message (and all subparts) to a file"
|
|
||||||
g = MimeGenerator(file)
|
|
||||||
g.flatten(self,unixfrom=unixfrom)
|
|
||||||
|
|
||||||
def as_bytes(self, unixfrom=False):
|
|
||||||
"Return the entire formatted message as a string."
|
|
||||||
fp = BytesIO()
|
|
||||||
self.dump(fp,unixfrom=unixfrom)
|
|
||||||
return fp.getvalue()
|
|
||||||
|
|
||||||
def getencoding(self):
|
|
||||||
return self.get('content-transfer-encoding',None)
|
|
||||||
|
|
||||||
# Decode body to stream according to transfer encoding, return encoding name
|
|
||||||
def decode(self,filt):
|
|
||||||
try:
|
|
||||||
filt.write(self.get_payload(decode=True))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return self.getencoding()
|
|
||||||
|
|
||||||
def get_payload_decoded(self):
|
|
||||||
return self.get_payload(decode=True)
|
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
|
||||||
rc = Message.__setitem__(self,name,value)
|
|
||||||
self.modified = True
|
|
||||||
if self.headerchange: self.headerchange(self,name,str(value))
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def __delitem__(self, name):
|
|
||||||
if self.headerchange: self.headerchange(self,name,None)
|
|
||||||
rc = Message.__delitem__(self,name)
|
|
||||||
self.modified = True
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def get_payload(self,i=None,decode=False):
|
|
||||||
msg = self.submsg
|
|
||||||
if msg is None:
|
|
||||||
t = self.get_content_type().lower()
|
|
||||||
if t == 'message/rfc822' or t.startswith('multipart/'):
|
|
||||||
msg = super().get_payload()
|
|
||||||
self.submsg = msg
|
|
||||||
if isinstance(msg,Message) and msg.ismodified():
|
|
||||||
self.set_payload([msg])
|
|
||||||
return Message.get_payload(self,i,decode)
|
|
||||||
|
|
||||||
def set_payload(self, val, charset=None):
|
|
||||||
self.modified = True
|
|
||||||
try:
|
|
||||||
val.seek(0)
|
|
||||||
val = val.read()
|
|
||||||
except: pass
|
|
||||||
Message.set_payload(self,val,charset)
|
|
||||||
self.submsg = None
|
|
||||||
|
|
||||||
def get_submsg(self):
|
|
||||||
t = self.get_content_type().lower()
|
|
||||||
if t == 'message/rfc822' or t.startswith('multipart/'):
|
|
||||||
if not self.submsg:
|
|
||||||
txt = self.get_payload()
|
|
||||||
if type(txt) is bytes:
|
|
||||||
self.submsg = email.message_from_bytes(txt,MimeMessage)
|
|
||||||
for part in self.submsg.walk():
|
|
||||||
part.modified = False
|
|
||||||
elif type(txt) is str:
|
|
||||||
txt = self.get_payload(decode=True)
|
|
||||||
self.submsg = email.message_from_string(txt,MimeMessage)
|
|
||||||
for part in self.submsg.walk():
|
|
||||||
part.modified = False
|
|
||||||
else:
|
|
||||||
self.submsg = txt[0]
|
|
||||||
return self.submsg
|
|
||||||
return None
|
|
||||||
|
|
||||||
def message_from_file(fp):
|
|
||||||
msg = message_from_binary_file(fp,MimeMessage)
|
|
||||||
for part in msg.walk():
|
|
||||||
part.modified = False
|
|
||||||
assert not msg.ismodified()
|
|
||||||
return msg
|
|
||||||
|
|
||||||
extlist = ''.join("""
|
|
||||||
ade,adp,asd,asx,asp,bas,bat,chm,cmd,com,cpl,crt,dll,exe,hlp,hta,inf,ins,isp,js,
|
|
||||||
jse,lnk,mdb,mde,msc,msi,msp,mst,ocx,pcd,pif,reg,scr,sct,shs,url,vb,vbe,vbs,wsc,
|
|
||||||
wsf,wsh
|
|
||||||
""".split())
|
|
||||||
bad_extensions = ['.' + x for x in extlist.split(',')]
|
|
||||||
|
|
||||||
def check_ext(name):
|
|
||||||
"Check a name for dangerous Winblows extensions."
|
|
||||||
if not name: return name
|
|
||||||
lname = name.lower()
|
|
||||||
for ext in bad_extensions:
|
|
||||||
if lname.endswith(ext): return name
|
|
||||||
return None
|
|
||||||
|
|
||||||
virus_msg = """This message appeared to contain a virus.
|
|
||||||
It was originally named '%s', and has been removed.
|
|
||||||
A copy of your original message was saved as '%s:%s'.
|
|
||||||
See your administrator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def check_name(msg,savname=None,ckname=check_ext,scan_zip=False):
|
|
||||||
"Replace attachment with a warning if its name is suspicious."
|
|
||||||
try:
|
|
||||||
for key,name in msg.getnames(scan_zip):
|
|
||||||
badname = ckname(name)
|
|
||||||
if badname:
|
|
||||||
if key == 'zipname':
|
|
||||||
badname = msg.get_filename()
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return Milter.CONTINUE
|
|
||||||
except zipfile.BadZipfile:
|
|
||||||
# a ZIP that is not a zip is very suspicious
|
|
||||||
badname = msg.get_filename()
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
msg.set_payload(virus_msg % (badname,hostname,savname))
|
|
||||||
del msg["content-type"]
|
|
||||||
del msg["content-disposition"]
|
|
||||||
del msg["content-transfer-encoding"]
|
|
||||||
name = "WARNING.TXT"
|
|
||||||
msg["Content-Type"] = "text/plain; name="+name
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def check_attachments(msg,check,lev=None):
|
|
||||||
"""Scan attachments.
|
|
||||||
msg MimeMessage
|
|
||||||
check function(MimeMessage): int
|
|
||||||
Return CONTINUE, REJECT, ACCEPT
|
|
||||||
"""
|
|
||||||
if msg.is_multipart():
|
|
||||||
if not lev: lev = []
|
|
||||||
lev.append(1)
|
|
||||||
if msg.get_content_type().endswith('/rfc822'):
|
|
||||||
foo = 1
|
|
||||||
for i in msg.get_payload():
|
|
||||||
print('chkm',lev,msg.get_content_type())
|
|
||||||
rc = check_attachments(i,check,lev=lev)
|
|
||||||
if rc != Milter.CONTINUE: return rc
|
|
||||||
lev[-1] += 1
|
|
||||||
return Milter.CONTINUE
|
|
||||||
print('chk',lev,msg.get_content_type())
|
|
||||||
return check(msg)
|
|
||||||
|
|
||||||
# save call context for Python without nested_scopes
|
|
||||||
class _defang:
|
|
||||||
|
|
||||||
def __init__(self,scan_html=True):
|
|
||||||
self.scan_html = scan_html
|
|
||||||
|
|
||||||
def _chk_name(self,msg):
|
|
||||||
rc = check_name(msg,self._savname,self._check,self.scan_zip)
|
|
||||||
if self.scan_html:
|
|
||||||
check_html(msg,self._savname) # remove scripts from HTML
|
|
||||||
if self.scan_rfc822:
|
|
||||||
msg = msg.get_submsg()
|
|
||||||
if isinstance(msg,Message):
|
|
||||||
return check_attachments(msg,self._chk_name)
|
|
||||||
return rc
|
|
||||||
|
|
||||||
def __call__(self,msg,savname=None,check=check_ext,scan_rfc822=True,
|
|
||||||
scan_zip=False):
|
|
||||||
"""Compatible entry point.
|
|
||||||
Replace all attachments with dangerous names."""
|
|
||||||
self._savname = savname
|
|
||||||
self._check = check
|
|
||||||
self.scan_rfc822 = scan_rfc822
|
|
||||||
self.scan_zip = scan_zip
|
|
||||||
check_attachments(msg,self._chk_name)
|
|
||||||
if msg.ismodified():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
# emulate old defang function
|
|
||||||
defang = _defang()
|
|
||||||
|
|
||||||
if sys.version < '3.0.0':
|
|
||||||
from sgmllib import SGMLParser as HTMLParser
|
|
||||||
else:
|
|
||||||
from Milter.sgmllib import SGMLParser as HTMLParser
|
|
||||||
|
|
||||||
import re
|
|
||||||
declname = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*')
|
|
||||||
declstringlit = re.compile(r'(\'[^\']*\'|"[^"]*")\s*')
|
|
||||||
|
|
||||||
class SGMLFilter(HTMLParser):
|
|
||||||
"""Parse HTML and pass through all constructs unchanged. It is intended for
|
|
||||||
derived classes to implement exceptional processing for selected cases.
|
|
||||||
"""
|
|
||||||
def __init__(self,out):
|
|
||||||
HTMLParser.__init__(self)
|
|
||||||
self.out = out
|
|
||||||
|
|
||||||
def handle_comment(self,comment):
|
|
||||||
self.out.write("<!--%s-->" % comment)
|
|
||||||
|
|
||||||
def unknown_starttag(self,tag,attr):
|
|
||||||
if hasattr(self,"get_starttag_text"):
|
|
||||||
self.out.write(self.get_starttag_text())
|
|
||||||
else:
|
|
||||||
self.out.write("<%s" % tag)
|
|
||||||
for (key,val) in attr:
|
|
||||||
self.out.write(' %s="%s"' % (key,val))
|
|
||||||
self.out.write('>')
|
|
||||||
|
|
||||||
def handle_data(self,data):
|
|
||||||
self.out.write(data)
|
|
||||||
|
|
||||||
def handle_entityref(self,ref):
|
|
||||||
self.out.write("&%s;" % ref)
|
|
||||||
|
|
||||||
def handle_charref(self,ref):
|
|
||||||
self.out.write("&#%s;" % ref)
|
|
||||||
|
|
||||||
def unknown_endtag(self,tag):
|
|
||||||
self.out.write("</%s>" % tag)
|
|
||||||
|
|
||||||
def handle_special(self,data):
|
|
||||||
self.out.write("<!%s>" % data)
|
|
||||||
|
|
||||||
def write(self,buf):
|
|
||||||
"Act like a writer. Why doesn't HTMLParser do this by default?"
|
|
||||||
self.feed(buf)
|
|
||||||
|
|
||||||
# Python-2.1 sgmllib rejects illegal declarations. Since various Microsoft
|
|
||||||
# products accept and output them, we need to pass them through -
|
|
||||||
# at least until we discover that MS will execute them.
|
|
||||||
# sgmlop-1.1 will not use this method, but calls handle_special to
|
|
||||||
# do what we want.
|
|
||||||
def parse_declaration(self, i):
|
|
||||||
rawdata = self.rawdata
|
|
||||||
n = len(rawdata)
|
|
||||||
j = i + 2
|
|
||||||
while j < n:
|
|
||||||
c = rawdata[j]
|
|
||||||
if c == ">":
|
|
||||||
# end of declaration syntax
|
|
||||||
self.handle_special(rawdata[i+2:j])
|
|
||||||
return j + 1
|
|
||||||
if c in "\"'":
|
|
||||||
m = declstringlit.match(rawdata, j)
|
|
||||||
if not m:
|
|
||||||
# incomplete or an error?
|
|
||||||
return -1
|
|
||||||
j = m.end()
|
|
||||||
elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
|
||||||
m = declname.match(rawdata, j)
|
|
||||||
if not m:
|
|
||||||
# incomplete or an error?
|
|
||||||
return -1
|
|
||||||
j = m.end()
|
|
||||||
else:
|
|
||||||
j += 1
|
|
||||||
# end of buffer between tokens
|
|
||||||
return -1
|
|
||||||
|
|
||||||
class HTMLScriptFilter(SGMLFilter):
|
|
||||||
"Remove scripts from an HTML document."
|
|
||||||
def __init__(self,out):
|
|
||||||
SGMLFilter.__init__(self,out)
|
|
||||||
self.ignoring = 0
|
|
||||||
self.modified = False
|
|
||||||
self.msg = "<!-- WARNING: embedded script removed -->"
|
|
||||||
def start_script(self,unused):
|
|
||||||
#print('beg script',unused)
|
|
||||||
self.ignoring += 1
|
|
||||||
self.modified = True
|
|
||||||
def end_script(self):
|
|
||||||
#print('end script')
|
|
||||||
self.ignoring -= 1
|
|
||||||
if not self.ignoring:
|
|
||||||
self.out.write(self.msg)
|
|
||||||
def handle_data(self,data):
|
|
||||||
if not self.ignoring: SGMLFilter.handle_data(self,data)
|
|
||||||
def handle_comment(self,comment):
|
|
||||||
if not self.ignoring: SGMLFilter.handle_comment(self,comment)
|
|
||||||
|
|
||||||
def check_html(msg,savname=None):
|
|
||||||
"Remove scripts from HTML attachments."
|
|
||||||
msgtype = msg.get_content_type().lower()
|
|
||||||
# check for more MSIE braindamage
|
|
||||||
if msgtype == 'application/octet-stream':
|
|
||||||
for (attr,name) in msg.getnames():
|
|
||||||
if name and name.lower().endswith(".htm"):
|
|
||||||
msgtype = 'text/html'
|
|
||||||
if msgtype == 'text/html':
|
|
||||||
out = StringIO()
|
|
||||||
htmlfilter = HTMLScriptFilter(out)
|
|
||||||
try:
|
|
||||||
htmlfilter.write(msg.get_payload(decode=True).decode())
|
|
||||||
htmlfilter.close()
|
|
||||||
#except sgmllib.SGMLParseError:
|
|
||||||
except:
|
|
||||||
mimetools.copyliteral(msg.get_payload(),open('debug.out','wb'))
|
|
||||||
htmlfilter.close()
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
msg.set_payload(
|
|
||||||
"An HTML attachment could not be parsed. The original is saved as '%s:%s'"
|
|
||||||
% (hostname,savname))
|
|
||||||
del msg["content-type"]
|
|
||||||
del msg["content-disposition"]
|
|
||||||
del msg["content-transfer-encoding"]
|
|
||||||
name = "WARNING.TXT"
|
|
||||||
msg["Content-Type"] = "text/plain; name="+name
|
|
||||||
return Milter.CONTINUE
|
|
||||||
if htmlfilter.modified:
|
|
||||||
msg.set_payload(out) # remove embedded scripts
|
|
||||||
del msg["content-transfer-encoding"]
|
|
||||||
encoders.encode_quopri(msg)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
def _list_attach(msg):
|
|
||||||
t = msg.get_content_type()
|
|
||||||
p = msg.get_payload(decode=True)
|
|
||||||
print(msg.get_filename(),msg.get_content_type(),type(p))
|
|
||||||
msg = msg.get_submsg()
|
|
||||||
if isinstance(msg,Message):
|
|
||||||
return check_attachments(msg,_list_attach)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
for fname in sys.argv[1:]:
|
|
||||||
with open(fname,'rb') as fp:
|
|
||||||
msg = message_from_file(fp)
|
|
||||||
email.iterators._structure(msg)
|
|
||||||
check_attachments(msg,_list_attach)
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Check Description Justification
|
|
||||||
E111 req indent 4 Creates more continuation lines
|
|
||||||
E114 req indent 4 cmnt Same
|
|
||||||
E231 req space after , makes calls like print() harder to read
|
|
||||||
E266 no ## Required by Doxygen
|
|
||||||
W291 trailing spaces in cmnt Needed for space preserving para reformat
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
ignore=`awk -F\\\\t '{ print $1 }' pep8.dat | tail -n +2`
|
|
||||||
a=(${ignore})
|
|
||||||
list=$(echo "${a[@]}"|tr '[ ]' '[,]')
|
|
||||||
echo python3 -m pep8 --ignore="$list" $@
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
diff -up ./Milter/utils.py.check ./Milter/utils.py
|
|
||||||
--- ./Milter/utils.py.check 2018-08-04 23:01:23.858668412 -0400
|
|
||||||
+++ ./Milter/utils.py 2018-08-04 23:01:39.460869588 -0400
|
|
||||||
@@ -68,10 +68,6 @@ def iniplist(ipaddr,iplist):
|
|
||||||
True
|
|
||||||
>>> iniplist('192.168.0.45',['192.168.0.*'])
|
|
||||||
True
|
|
||||||
- >>> iniplist('4.2.2.2',['b.resolvers.Level3.net'])
|
|
||||||
- True
|
|
||||||
- >>> iniplist('2606:2800:220:1::',['example.com/40'])
|
|
||||||
- True
|
|
||||||
>>> iniplist('4.2.2.2',['nothing.example.com'])
|
|
||||||
False
|
|
||||||
>>> iniplist('2001:610:779:0:223:6cff:fe9a:9cf3',['127.0.0.1','172.20.1.0/24','2001:610:779::/48'])
|
|
||||||
diff -up ./test.py.check ./test.py
|
|
||||||
--- ./test.py.check 2018-08-04 23:04:58.609420815 -0400
|
|
||||||
+++ ./test.py 2018-08-04 23:05:40.070949438 -0400
|
|
||||||
@@ -14,6 +14,8 @@ def suite():
|
|
||||||
return s
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
+ import sys
|
|
||||||
try: os.remove('test/milter.log')
|
|
||||||
except: pass
|
|
||||||
- unittest.TextTestRunner().run(suite())
|
|
||||||
+ rc = unittest.TextTestRunner().run(suite())
|
|
||||||
+ sys.exit(len(rc.failures))
|
|
||||||
-300
@@ -1,300 +0,0 @@
|
|||||||
# we don't want to provide private python extension libs
|
|
||||||
%global sum Python interface to sendmail milter API
|
|
||||||
%global __provides_exclude_from ^(%{python2_sitearch})/.*\\.so$
|
|
||||||
%if 0%{?epel} == 7
|
|
||||||
%global python3 python36
|
|
||||||
%else
|
|
||||||
%global python3 python3
|
|
||||||
%endif
|
|
||||||
|
|
||||||
Summary: %{sum}
|
|
||||||
Name: python-pymilter
|
|
||||||
Version: 1.0.4
|
|
||||||
Release: 1%{?dist}
|
|
||||||
Url: http://bmsi.com/pymilter
|
|
||||||
Source: https://github.com/sdgathman/pymilter/archive/pymilter-%{version}.tar.gz
|
|
||||||
#Source1: tmpfiles-python-pymilter.conf
|
|
||||||
# remove unit tests that require network for check
|
|
||||||
Patch: pymilter-check.patch
|
|
||||||
License: GPLv2+
|
|
||||||
Group: Development/Libraries
|
|
||||||
BuildRequires: python2-devel, %{python3}-devel, sendmail-devel >= 8.13
|
|
||||||
# python-2.6.4 gets RuntimeError: not holding the import lock
|
|
||||||
# Need python2.6 specific pydns, not the version for system python
|
|
||||||
BuildRequires: gcc
|
|
||||||
|
|
||||||
%global _description\
|
|
||||||
This is a python extension module to enable python scripts to\
|
|
||||||
attach to sendmail's libmilter functionality. Additional python\
|
|
||||||
modules provide for navigating and modifying MIME parts, sending\
|
|
||||||
DSNs, and doing CBV.
|
|
||||||
|
|
||||||
%description %_description
|
|
||||||
|
|
||||||
%package -n python2-pymilter
|
|
||||||
Summary: %{sum}
|
|
||||||
%if 0%{?epel} >= 6
|
|
||||||
Requires: python-pydns
|
|
||||||
%else
|
|
||||||
Requires: python2-pydns
|
|
||||||
%endif
|
|
||||||
Requires: %{name}-common = %{version}-%{release}
|
|
||||||
%{?python_provide:%python_provide python2-pymilter}
|
|
||||||
|
|
||||||
%description -n python2-pymilter %_description
|
|
||||||
|
|
||||||
%package -n %{python3}-pymilter
|
|
||||||
Summary: %{sum}
|
|
||||||
%if 0%{?fedora} >= 26
|
|
||||||
Requires: %{python3}-py3dns
|
|
||||||
%endif
|
|
||||||
Requires: %{name}-common = %{version}-%{release}
|
|
||||||
%{?python_provide:%python_provide %{python3}-pymilter}
|
|
||||||
|
|
||||||
%description -n %{python3}-pymilter %_description
|
|
||||||
|
|
||||||
%package common
|
|
||||||
Summary: Common files and directories for python milters
|
|
||||||
BuildArch: noarch
|
|
||||||
|
|
||||||
%description common
|
|
||||||
Common files and directories used for python milters
|
|
||||||
|
|
||||||
%package selinux
|
|
||||||
Summary: SELinux policy module for pymilter
|
|
||||||
Group: System Environment/Base
|
|
||||||
Requires: policycoreutils, selinux-policy-targeted
|
|
||||||
Requires: %{name} = %{version}-%{release}
|
|
||||||
BuildArch: noarch
|
|
||||||
BuildRequires: policycoreutils, checkpolicy, selinux-policy-devel
|
|
||||||
%if 0%{?epel} >= 6
|
|
||||||
BuildRequires: policycoreutils-python
|
|
||||||
%else
|
|
||||||
BuildRequires: policycoreutils-python-utils
|
|
||||||
%endif
|
|
||||||
|
|
||||||
%description selinux
|
|
||||||
Give sendmail_t additional access to stream sockets used to communicate
|
|
||||||
with milters.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -n pymilter-pymilter-%{version}
|
|
||||||
#patch -p1 -b .check
|
|
||||||
|
|
||||||
%build
|
|
||||||
%py2_build
|
|
||||||
%py3_build
|
|
||||||
checkmodule -m -M -o pymilter.mod pymilter.te
|
|
||||||
semodule_package -o pymilter.pp -m pymilter.mod
|
|
||||||
|
|
||||||
%install
|
|
||||||
%py2_install
|
|
||||||
%py3_install
|
|
||||||
|
|
||||||
mkdir -p %{buildroot}/run/milter
|
|
||||||
mkdir -p %{buildroot}%{_localstatedir}/log/milter
|
|
||||||
mkdir -p %{buildroot}%{_libexecdir}/milter
|
|
||||||
#mkdir -p %{buildroot}%{_prefix}/lib/tmpfiles.d
|
|
||||||
#install -m 0644 %{SOURCE1} %{buildroot}%{_prefix}/lib/tmpfiles.d/%{name}.conf
|
|
||||||
|
|
||||||
# install selinux modules
|
|
||||||
mkdir -p %{buildroot}%{_datadir}/selinux/targeted
|
|
||||||
cp -p pymilter.pp %{buildroot}%{_datadir}/selinux/targeted
|
|
||||||
|
|
||||||
%check
|
|
||||||
py2path=$(ls -d build/lib.linux-*-2.*)
|
|
||||||
py3path=$(ls -d build/lib.linux-*-3.*)
|
|
||||||
PYTHONPATH=${py2path}:. python2 test.py &&
|
|
||||||
PYTHONPATH=${py3path}:. python3 test.py
|
|
||||||
|
|
||||||
%files -n python2-pymilter
|
|
||||||
%license COPYING
|
|
||||||
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
|
|
||||||
%{python2_sitearch}/*
|
|
||||||
|
|
||||||
%files -n %{python3}-pymilter
|
|
||||||
%license COPYING
|
|
||||||
%doc README ChangeLog NEWS TODO CREDITS sample.py milter-template.py
|
|
||||||
%{python3_sitearch}/*
|
|
||||||
|
|
||||||
%files common
|
|
||||||
%dir %{_libexecdir}/milter
|
|
||||||
%{_prefix}/lib/tmpfiles.d/%{name}.conf
|
|
||||||
%dir %attr(0755,mail,mail) %{_localstatedir}/log/milter
|
|
||||||
%dir %attr(0755,mail,mail) /run/milter
|
|
||||||
|
|
||||||
%files selinux
|
|
||||||
%doc pymilter.te
|
|
||||||
%{_datadir}/selinux/targeted/*
|
|
||||||
|
|
||||||
%post selinux
|
|
||||||
%{_sbindir}/semodule -s targeted -i %{_datadir}/selinux/targeted/pymilter.pp \
|
|
||||||
&>/dev/null || :
|
|
||||||
|
|
||||||
%postun selinux
|
|
||||||
if [ $1 -eq 0 ] ; then
|
|
||||||
%{_sbindir}/semodule -s targeted -r pymilter &> /dev/null || :
|
|
||||||
fi
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* Wed Apr 17 2019 Stuart Gathman <stuart@gathman.org> - 1.0.4-1
|
|
||||||
- New upstream release: cleanup unused files, additional platform support
|
|
||||||
- Minor doc updates
|
|
||||||
|
|
||||||
* Sun Dec 23 2018 Stuart Gathman <stuart@gathman.org> - 1.0.3-1
|
|
||||||
- New upstream release
|
|
||||||
- patch step for python3 no longer required in build
|
|
||||||
|
|
||||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-4
|
|
||||||
- Add unit tests to %%check
|
|
||||||
|
|
||||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-3
|
|
||||||
- use libexec instead of libdir
|
|
||||||
|
|
||||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-2
|
|
||||||
- add python34 subpackage on el7
|
|
||||||
|
|
||||||
* Sat Aug 4 2018 Stuart Gathman <stuart@gathman.org> - 1.0.2-1
|
|
||||||
- build for both python2 and python3
|
|
||||||
- add selinux policy allowing sendmail_t access to milters
|
|
||||||
|
|
||||||
* Tue Jul 17 2018 Miro Hrončok <mhroncok@redhat.com> - 1.0-13
|
|
||||||
- Update Python macros to new packaging standards
|
|
||||||
(See https://fedoraproject.org/wiki/Changes/Move_usr_bin_python_into_separate_package)
|
|
||||||
|
|
||||||
* Sat Jul 14 2018 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-12
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
|
|
||||||
|
|
||||||
* Fri Feb 09 2018 Iryna Shcherbina <ishcherb@redhat.com> - 1.0-11
|
|
||||||
- Update Python 2 dependency declarations to new packaging standards
|
|
||||||
(See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3)
|
|
||||||
|
|
||||||
* Fri Feb 09 2018 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-10
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
|
|
||||||
|
|
||||||
* Fri Feb 09 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 1.0-9
|
|
||||||
- Escape macros in %%changelog
|
|
||||||
|
|
||||||
* Sat Aug 19 2017 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> - 1.0-8
|
|
||||||
- Python 2 binary package renamed to python2-pymilter
|
|
||||||
See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3
|
|
||||||
|
|
||||||
* Thu Aug 03 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-7
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Binutils_Mass_Rebuild
|
|
||||||
|
|
||||||
* Thu Jul 27 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-6
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
|
|
||||||
>>>>>>> 021796e51e5919812f1c300d1830ef9ed378db2d
|
|
||||||
|
|
||||||
* Sat Feb 11 2017 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-5
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_26_Mass_Rebuild
|
|
||||||
|
|
||||||
* Tue Jul 19 2016 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.0-4
|
|
||||||
- https://fedoraproject.org/wiki/Changes/Automatic_Provides_for_Python_RPM_Packages
|
|
||||||
|
|
||||||
* Thu Feb 04 2016 Fedora Release Engineering <releng@fedoraproject.org> - 1.0-3
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_24_Mass_Rebuild
|
|
||||||
|
|
||||||
* Thu Jun 18 2015 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 1.0-2
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_23_Mass_Rebuild
|
|
||||||
|
|
||||||
* Sat Sep 27 2014 Paul Wouters <pwouters@redhat.com> - 1.0-1
|
|
||||||
- Updated to 1.0
|
|
||||||
- Use tmpfiles and /run
|
|
||||||
|
|
||||||
* Sun Aug 17 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.8-6
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_22_Mass_Rebuild
|
|
||||||
|
|
||||||
* Sat Jun 07 2014 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.9.8-5
|
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild
|
|
||||||
|
|
||||||
* Fri Jan 10 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-4
|
|
||||||
- Add COPYING
|
|
||||||
- Fix buildroot macros and dist macro
|
|
||||||
|
|
||||||
* Fri Jan 10 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-3
|
|
||||||
- rebuilt with proper file permission
|
|
||||||
|
|
||||||
* Tue Jan 07 2014 Paul Wouters <pwouters@redhat.com> - 0.9.8-2
|
|
||||||
- Fixup for fedora release
|
|
||||||
|
|
||||||
* Sat Mar 9 2013 Stuart Gathman <stuart@bmsi.com> 0.9.8-1
|
|
||||||
- Add Milter.test module for unit testing milters.
|
|
||||||
- Fix typo that prevented setsymlist from being active.
|
|
||||||
- Change untrapped exception message to:
|
|
||||||
- "pymilter: untrapped exception in milter app"
|
|
||||||
|
|
||||||
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.7-1
|
|
||||||
- Raise RuntimeError when result != CONTINUE for @noreply and @nocallback
|
|
||||||
- Remove redundant table in miltermodule
|
|
||||||
- Fix CNAME chain duplicating TXT records in Milter.dns (from pyspf).
|
|
||||||
|
|
||||||
* Sat Feb 25 2012 Stuart Gathman <stuart@bmsi.com> 0.9.6-1
|
|
||||||
- Raise ValueError on unescaped '%%' passed to setreply
|
|
||||||
- Grace time at end of Greylist window
|
|
||||||
|
|
||||||
* Fri Aug 19 2011 Stuart Gathman <stuart@bmsi.com> 0.9.5-1
|
|
||||||
- Print milter.error for invalid callback return type.
|
|
||||||
(Since stacktrace is empty, the TypeError exception is confusing.)
|
|
||||||
- Fix milter-template.py
|
|
||||||
- Tweak Milter.utils.addr2bin and Milter.dynip to handle IP6
|
|
||||||
|
|
||||||
* Tue Mar 02 2010 Stuart Gathman <stuart@bmsi.com> 0.9.4-1
|
|
||||||
- Handle IP6 in Milter.utils.iniplist()
|
|
||||||
- python-2.6
|
|
||||||
|
|
||||||
* Thu Jul 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.3-1
|
|
||||||
- Handle source route in Milter.utils.parse_addr()
|
|
||||||
- Fix default arg in chgfrom.
|
|
||||||
- Disable negotiate callback for libmilter < 8.14.3 (1,0,1)
|
|
||||||
|
|
||||||
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-3
|
|
||||||
- Change result of @noreply callbacks to NOREPLY when so negotiated.
|
|
||||||
|
|
||||||
* Tue Jun 02 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-2
|
|
||||||
- Cache callback negotiation
|
|
||||||
|
|
||||||
* Thu May 28 2009 Stuart Gathman <stuart@bmsi.com> 0.9.2-1
|
|
||||||
- Add new callback support: data,negotiate,unknown
|
|
||||||
- Auto-negotiate protocol steps
|
|
||||||
|
|
||||||
* Thu Feb 05 2009 Stuart Gathman <stuart@bmsi.com> 0.9.1-1
|
|
||||||
- Fix missing address of optional param to addrcpt
|
|
||||||
|
|
||||||
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-4
|
|
||||||
- Stop using INSTALLED_FILES to make Fedora happy
|
|
||||||
- Remove config flag from start.sh glue
|
|
||||||
- Own /var/log/milter
|
|
||||||
- Use _localstatedir
|
|
||||||
|
|
||||||
* Wed Jan 07 2009 Stuart Gathman <stuart@bmsi.com> 0.9.0-2
|
|
||||||
- Changes to meet Fedora standards
|
|
||||||
|
|
||||||
* Mon Nov 24 2008 Stuart Gathman <stuart@bmsi.com> 0.9.0-1
|
|
||||||
- Split pymilter into its own CVS module
|
|
||||||
- Support chgfrom and addrcpt_par
|
|
||||||
- Support NS records in Milter.dns
|
|
||||||
|
|
||||||
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-2
|
|
||||||
- /var/run/milter directory must be owned by mail
|
|
||||||
|
|
||||||
* Mon Aug 25 2008 Stuart Gathman <stuart@bmsi.com> 0.8.10-1
|
|
||||||
- improved parsing into email and fullname (still 2 self test failures)
|
|
||||||
- implement no-DSN CBV, reduce full DSNs
|
|
||||||
|
|
||||||
* Mon Sep 24 2007 Stuart Gathman <stuart@bmsi.com> 0.8.9-1
|
|
||||||
- Use ifarch hack to build milter and milter-spf packages as noarch
|
|
||||||
- Remove spf dependency from dsn.py, add dns.py
|
|
||||||
|
|
||||||
* Fri Jan 05 2007 Stuart Gathman <stuart@bmsi.com> 0.8.8-1
|
|
||||||
- move AddrCache, parse_addr, iniplist to Milter package
|
|
||||||
- move parse_header to Milter.utils
|
|
||||||
- fix plock for missing source and can't change owner/group
|
|
||||||
- split out pymilter and pymilter-spf packages
|
|
||||||
- move milter apps to /usr/lib/pymilter
|
|
||||||
|
|
||||||
* Sat Nov 04 2006 Stuart Gathman <stuart@bmsi.com> 0.8.7-1
|
|
||||||
- SPF moved to pyspf RPM
|
|
||||||
|
|
||||||
* Tue May 23 2006 Stuart Gathman <stuart@bmsi.com> 0.8.6-2
|
|
||||||
- Support CBV timeout
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
module pymilter 1.0;
|
|
||||||
|
|
||||||
require {
|
|
||||||
type sendmail_t;
|
|
||||||
type var_run_t;
|
|
||||||
type initrc_t;
|
|
||||||
class sock_file { write getattr };
|
|
||||||
class unix_stream_socket connectto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#============= sendmail_t ==============
|
|
||||||
allow sendmail_t initrc_t:unix_stream_socket connectto;
|
|
||||||
allow sendmail_t var_run_t:sock_file { write getattr };
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
# A simple milter.
|
|
||||||
|
|
||||||
# Author: Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2001 Business Management Systems, Inc.
|
|
||||||
# This code is under GPL. See COPYING for details.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
try:
|
|
||||||
from io import BytesIO
|
|
||||||
except:
|
|
||||||
from StringIO import StringIO as BytesIO
|
|
||||||
import mime
|
|
||||||
import Milter
|
|
||||||
import tempfile
|
|
||||||
from time import strftime
|
|
||||||
#import syslog
|
|
||||||
|
|
||||||
#syslog.openlog('milter')
|
|
||||||
|
|
||||||
class sampleMilter(Milter.Milter):
|
|
||||||
"Milter to replace attachments poisonous to Windows with a WARNING message."
|
|
||||||
|
|
||||||
def log(self,*msg):
|
|
||||||
print("%s [%d]" % (strftime('%Y%b%d %H:%M:%S'),self.id),end=None)
|
|
||||||
for i in msg:
|
|
||||||
try:
|
|
||||||
print(i,end=None)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
s = i.encode(encoding='utf-8',errors='surrogateescape')
|
|
||||||
print(s,end=None)
|
|
||||||
print()
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.tempname = None
|
|
||||||
self.mailfrom = None
|
|
||||||
self.fp = None
|
|
||||||
self.bodysize = 0
|
|
||||||
self.id = Milter.uniqueID()
|
|
||||||
self.user = None
|
|
||||||
|
|
||||||
# multiple messages can be received on a single connection
|
|
||||||
# envfrom (MAIL FROM in the SMTP protocol) seems to mark the start
|
|
||||||
# of each message.
|
|
||||||
@Milter.symlist('{auth_authen}')
|
|
||||||
@Milter.noreply
|
|
||||||
def envfrom(self,f,*str):
|
|
||||||
"start of MAIL transaction"
|
|
||||||
self.fp = BytesIO()
|
|
||||||
self.tempname = None
|
|
||||||
self.mailfrom = f
|
|
||||||
self.bodysize = 0
|
|
||||||
self.user = self.getsymval('{auth_authen}')
|
|
||||||
self.auth_type = self.getsymval('{auth_type}')
|
|
||||||
if self.user:
|
|
||||||
self.log("user",self.user,"sent mail from",f,str)
|
|
||||||
else:
|
|
||||||
self.log("mail from",f,str)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def envrcpt(self,to,*str):
|
|
||||||
# mail to MAILER-DAEMON is generally spam that bounced
|
|
||||||
if to.startswith('<MAILER-DAEMON@'):
|
|
||||||
self.log('DISCARD: RCPT TO:',to,str)
|
|
||||||
return Milter.DISCARD
|
|
||||||
self.log("rcpt to",to,str)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
@Milter.decode('bytes')
|
|
||||||
def header(self,name,val):
|
|
||||||
lname = name.lower()
|
|
||||||
if lname == 'subject':
|
|
||||||
|
|
||||||
# even if we wanted the Taiwanese spam, we can't read Chinese
|
|
||||||
# (delete if you read chinese mail)
|
|
||||||
#print('val=',val.encode(errors='surrogateescape'))
|
|
||||||
print('val=',val)
|
|
||||||
if val.startswith(b'=?big5') or val.startswith(b'=?ISO-2022-JP'):
|
|
||||||
self.log('REJECT: %s: %s' % (name,val))
|
|
||||||
#self.setreply('550','','Go away spammer')
|
|
||||||
return Milter.REJECT
|
|
||||||
|
|
||||||
# check for common spam keywords
|
|
||||||
if val.find(b"$$$") >= 0 or val.find(b"XXX") >= 0 \
|
|
||||||
or val.find(b"!!!") >= 0 or val.find(b"FREE") >= 0:
|
|
||||||
self.log('REJECT: %s: %s' % (name,val))
|
|
||||||
#self.setreply('550','','Go away spammer')
|
|
||||||
return Milter.REJECT
|
|
||||||
|
|
||||||
# check for spam that pretends to be legal
|
|
||||||
lval = val.lower()
|
|
||||||
if lval.startswith(b"adv:") or lval.startswith(b"adv.") \
|
|
||||||
or lval.find(b'viagra') >= 0:
|
|
||||||
self.log('REJECT: %s: %s' % (name,val))
|
|
||||||
return Milter.REJECT
|
|
||||||
|
|
||||||
# check for invalid message id
|
|
||||||
if lname == 'message-id' and len(val) < 4:
|
|
||||||
self.log('REJECT: %s: %s' % (name,val))
|
|
||||||
#self.setreply('550','','Go away spammer')
|
|
||||||
return Milter.REJECT
|
|
||||||
|
|
||||||
# check for common bulk mailers
|
|
||||||
if lname == 'x-mailer' and \
|
|
||||||
val.lower() in (b'direct email',b'calypso',b'mail bomber'):
|
|
||||||
self.log('REJECT: %s: %s' % (name,val))
|
|
||||||
#self.setreply('550','','Go away spammer')
|
|
||||||
return Milter.REJECT
|
|
||||||
|
|
||||||
# log selected headers
|
|
||||||
if lname in ('subject','x-mailer'):
|
|
||||||
self.log('%s: %s' % (name,val))
|
|
||||||
if self.fp:
|
|
||||||
self.fp.write(b"%s: %s\n" % (name.encode(),val)) # add header to buffer
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def eoh(self):
|
|
||||||
if not self.fp: return Milter.TEMPFAIL # not seen by envfrom
|
|
||||||
self.fp.write(b'\n')
|
|
||||||
self.fp.seek(0)
|
|
||||||
# copy headers to a temp file for scanning the body
|
|
||||||
headers = self.fp.getvalue()
|
|
||||||
self.fp.close()
|
|
||||||
self.tempname = fname = tempfile.mktemp(".defang")
|
|
||||||
self.fp = open(fname,"w+b")
|
|
||||||
self.fp.write(headers) # IOError (e.g. disk full) causes TEMPFAIL
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def body(self,chunk): # copy body to temp file
|
|
||||||
if self.fp:
|
|
||||||
self.fp.write(chunk) # IOError causes TEMPFAIL in milter
|
|
||||||
self.bodysize += len(chunk)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def _headerChange(self,msg,name,value):
|
|
||||||
if value: # add header
|
|
||||||
self.addheader(name,value)
|
|
||||||
else: # delete all headers with name
|
|
||||||
h = msg.getheaders(name)
|
|
||||||
cnt = len(h)
|
|
||||||
for i in range(cnt,0,-1):
|
|
||||||
self.chgheader(name,i-1,'')
|
|
||||||
|
|
||||||
def eom(self):
|
|
||||||
if not self.fp: return Milter.ACCEPT
|
|
||||||
self.fp.seek(0)
|
|
||||||
msg = mime.message_from_file(self.fp)
|
|
||||||
msg.headerchange = self._headerChange
|
|
||||||
if not mime.defang(msg,self.tempname):
|
|
||||||
os.remove(self.tempname)
|
|
||||||
self.tempname = None # prevent re-removal
|
|
||||||
self.log("eom")
|
|
||||||
return Milter.ACCEPT # no suspicious attachments
|
|
||||||
self.log("Temp file:",self.tempname)
|
|
||||||
self.tempname = None # prevent removal of original message copy
|
|
||||||
# copy defanged message to a temp file
|
|
||||||
with tempfile.TemporaryFile() as out:
|
|
||||||
msg.dump(out)
|
|
||||||
out.seek(0)
|
|
||||||
msg = mime.message_from_file(out)
|
|
||||||
fp = BytesIO(msg.as_bytes().split(b'\n\n',1)[1])
|
|
||||||
while 1:
|
|
||||||
buf = fp.read(8192)
|
|
||||||
if len(buf) == 0: break
|
|
||||||
self.replacebody(buf) # feed modified message to sendmail
|
|
||||||
return Milter.ACCEPT # ACCEPT modified message
|
|
||||||
return Milter.TEMPFAIL
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
sys.stdout.flush() # make log messages visible
|
|
||||||
if self.tempname:
|
|
||||||
os.remove(self.tempname) # remove in case session aborted
|
|
||||||
if self.fp:
|
|
||||||
self.fp.close()
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def abort(self):
|
|
||||||
self.log("abort after %d body chars" % self.bodysize)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
#tempfile.tempdir = "/var/log/milter"
|
|
||||||
#socketname = "/var/log/milter/pythonsock"
|
|
||||||
socketname = os.getenv("HOME") + "/pythonsock"
|
|
||||||
Milter.factory = sampleMilter
|
|
||||||
Milter.set_flags(Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS)
|
|
||||||
print("""To use this with sendmail, add the following to sendmail.cf:
|
|
||||||
|
|
||||||
O InputMailFilters=pythonfilter
|
|
||||||
Xpythonfilter, S=local:%s
|
|
||||||
|
|
||||||
See the sendmail README for libmilter.
|
|
||||||
sample milter startup""" % socketname)
|
|
||||||
sys.stdout.flush()
|
|
||||||
Milter.runmilter("pythonfilter",socketname,240)
|
|
||||||
print("sample milter shutdown")
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[bdist_rpm]
|
|
||||||
python=python3
|
|
||||||
doc_files=README NEWS TODO COPYING CREDITS
|
|
||||||
packager=Stuart D. Gathman <stuart@gathman.org>
|
|
||||||
release=1
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from setuptools import setup, Extension
|
|
||||||
|
|
||||||
if sys.version < '2.6.5':
|
|
||||||
sys.exit('ERROR: Sorry, python 2.6.5 is required for this module.')
|
|
||||||
|
|
||||||
with open("README.md", "r") as fh:
|
|
||||||
long_description = fh.read()
|
|
||||||
|
|
||||||
# FIXME: on some versions of sendmail, smutil is renamed to sm.
|
|
||||||
# On slackware and debian, leave it out entirely. It depends
|
|
||||||
# on how libmilter was built by the sendmail package.
|
|
||||||
#libs = ["milter", "smutil"]
|
|
||||||
libs = ["milter"]
|
|
||||||
libdirs = ["/usr/lib/libmilter"] # needed for Debian
|
|
||||||
modules = ["mime"]
|
|
||||||
|
|
||||||
# NOTE: importing Milter to obtain version fails when milter.so not built
|
|
||||||
setup(name = "pymilter", version = '1.0.5',
|
|
||||||
description="Python interface to sendmail milter API",
|
|
||||||
long_description=long_description,
|
|
||||||
long_description_content_type='text/markdown',
|
|
||||||
author="Jim Niemira",
|
|
||||||
author_email="urmane@urmane.org",
|
|
||||||
maintainer="Stuart D. Gathman",
|
|
||||||
maintainer_email="stuart@gathman.org",
|
|
||||||
license="GPL",
|
|
||||||
url="https://www.pymilter.org/",
|
|
||||||
py_modules=modules,
|
|
||||||
packages = ['Milter'],
|
|
||||||
ext_modules=[
|
|
||||||
Extension("milter", ["miltermodule.c"],
|
|
||||||
library_dirs=libdirs,
|
|
||||||
libraries=libs,
|
|
||||||
# set MAX_ML_REPLY to 1 for sendmail < 8.13
|
|
||||||
define_macros = [ ('MAX_ML_REPLY',32) ],
|
|
||||||
# save lots of debugging time testing rfc2553 compliance
|
|
||||||
extra_compile_args = [ "-Werror=implicit-function-declaration" ]
|
|
||||||
),
|
|
||||||
],
|
|
||||||
keywords = ['sendmail','milter'],
|
|
||||||
classifiers = [
|
|
||||||
'Development Status :: 5 - Production/Stable',
|
|
||||||
'Environment :: No Input/Output (Daemon)',
|
|
||||||
'Intended Audience :: System Administrators',
|
|
||||||
'License :: OSI Approved :: GNU General Public License (GPL)',
|
|
||||||
'Natural Language :: English',
|
|
||||||
'Operating System :: POSIX',
|
|
||||||
'Programming Language :: Python',
|
|
||||||
'Topic :: Communications :: Email :: Mail Transport Agents',
|
|
||||||
'Topic :: Communications :: Email :: Filters'
|
|
||||||
]
|
|
||||||
)
|
|
||||||
-194
@@ -1,194 +0,0 @@
|
|||||||
## To roll your own milter, create a class that extends Milter.
|
|
||||||
# This is a useless example to show basic features of Milter.
|
|
||||||
# See the pymilter project at https://pymilter.org based
|
|
||||||
# on Sendmail's milter API
|
|
||||||
# This code is open-source on the same terms as Python.
|
|
||||||
|
|
||||||
## Milter calls methods of your class at milter events.
|
|
||||||
## Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
|
|
||||||
## You can also add/del recipients, replacebody, add/del headers, etc.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
import Milter
|
|
||||||
try:
|
|
||||||
from StringIO import StringIO as BytesIO
|
|
||||||
except:
|
|
||||||
from io import BytesIO
|
|
||||||
import time
|
|
||||||
import email
|
|
||||||
from email import message_from_binary_file
|
|
||||||
from email import policy
|
|
||||||
import mimetypes
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from socket import AF_INET, AF_INET6
|
|
||||||
from Milter.utils import parse_addr
|
|
||||||
if True:
|
|
||||||
# for logging process - usually not needed
|
|
||||||
from multiprocessing import Process as Thread, Queue
|
|
||||||
else:
|
|
||||||
from threading import Thread
|
|
||||||
from Queue import Queue
|
|
||||||
|
|
||||||
logq = None
|
|
||||||
|
|
||||||
class myMilter(Milter.Base):
|
|
||||||
|
|
||||||
def __init__(self): # A new instance with each new connection.
|
|
||||||
self.id = Milter.uniqueID() # Integer incremented with each call.
|
|
||||||
|
|
||||||
# each connection runs in its own thread and has its own myMilter
|
|
||||||
# instance. Python code must be thread safe. This is trivial if only stuff
|
|
||||||
# in myMilter instances is referenced.
|
|
||||||
@Milter.noreply
|
|
||||||
def connect(self, IPname, family, hostaddr):
|
|
||||||
# (self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
|
|
||||||
# (self, 'ip6.mxout.example.com', AF_INET6,
|
|
||||||
# ('3ffe:80e8:d8::1', 4720, 1, 0) )
|
|
||||||
self.IP = hostaddr[0]
|
|
||||||
self.port = hostaddr[1]
|
|
||||||
if family == AF_INET6:
|
|
||||||
self.flow = hostaddr[2]
|
|
||||||
self.scope = hostaddr[3]
|
|
||||||
else:
|
|
||||||
self.flow = None
|
|
||||||
self.scope = None
|
|
||||||
self.IPname = IPname # Name from a reverse IP lookup
|
|
||||||
self.H = None
|
|
||||||
self.fp = None
|
|
||||||
self.receiver = self.getsymval('j')
|
|
||||||
self.log("connect from %s at %s" % (IPname, hostaddr) )
|
|
||||||
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
|
|
||||||
## def hello(self,hostname):
|
|
||||||
def hello(self, heloname):
|
|
||||||
# (self, 'mailout17.dallas.texas.example.com')
|
|
||||||
self.H = heloname
|
|
||||||
self.log("HELO %s" % heloname)
|
|
||||||
if heloname.find('.') < 0: # illegal helo name
|
|
||||||
# NOTE: example only - too many real braindead clients to reject on this
|
|
||||||
self.setreply('550','5.7.1','Sheesh people! Use a proper helo name!')
|
|
||||||
return Milter.REJECT
|
|
||||||
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
## def envfrom(self,f,*str):
|
|
||||||
def envfrom(self, mailfrom, *str):
|
|
||||||
self.F = mailfrom
|
|
||||||
self.R = [] # list of recipients
|
|
||||||
self.fromparms = Milter.dictfromlist(str) # ESMTP parms
|
|
||||||
self.user = self.getsymval('{auth_authen}') # authenticated user
|
|
||||||
self.log("mail from:", mailfrom, *str)
|
|
||||||
# NOTE: self.fp is only an *internal* copy of message data. You
|
|
||||||
# must use addheader, chgheader, replacebody to change the message
|
|
||||||
# on the MTA.
|
|
||||||
self.fp = BytesIO()
|
|
||||||
self.canon_from = '@'.join(parse_addr(mailfrom))
|
|
||||||
self.fp.write(b'From %s %s\n' % (self.canon_from.encode(),
|
|
||||||
time.ctime().encode()))
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
|
|
||||||
## def envrcpt(self, to, *str):
|
|
||||||
@Milter.noreply
|
|
||||||
def envrcpt(self, to, *str):
|
|
||||||
rcptinfo = to,Milter.dictfromlist(str)
|
|
||||||
self.R.append(rcptinfo)
|
|
||||||
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
|
|
||||||
@Milter.noreply
|
|
||||||
def header(self, name, hval):
|
|
||||||
self.fp.write(b'%s: %s\n' % (name.encode(),hval.encode())) # add header to buffer
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
@Milter.noreply
|
|
||||||
def eoh(self):
|
|
||||||
self.fp.write(b'\n') # terminate headers
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
@Milter.noreply
|
|
||||||
def body(self, chunk):
|
|
||||||
self.fp.write(chunk)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def eom(self):
|
|
||||||
self.fp.seek(0)
|
|
||||||
msg = email.message_from_binary_file(self.fp, policy=policy.default)
|
|
||||||
|
|
||||||
#example on how to iterate through attachments
|
|
||||||
for attachment in msg.iter_attachments():
|
|
||||||
#attachment holds the attachment object so that it can be used with a new MIMEMultipart() message
|
|
||||||
self.log("Attachment filename is %s" % (attachment.get_filename(),))
|
|
||||||
self.log("Attachment content/type is %s" % (attachment.get_content_type(),))
|
|
||||||
data = attachment.get_content()
|
|
||||||
self.log("Attachment content is %s" % (data,))
|
|
||||||
|
|
||||||
# many milter functions can only be called from eom()
|
|
||||||
# example of adding a Bcc:
|
|
||||||
self.addrcpt('<%s>' % 'spy@example.com')
|
|
||||||
return Milter.ACCEPT
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
# always called, even when abort is called. Clean up
|
|
||||||
# any external resources here.
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def abort(self):
|
|
||||||
# client disconnected prematurely
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
## === Support Functions ===
|
|
||||||
|
|
||||||
def log(self,*msg):
|
|
||||||
t = (msg,self.id,time.time())
|
|
||||||
if logq:
|
|
||||||
logq.put(t)
|
|
||||||
else:
|
|
||||||
# logmsg(*t)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def logmsg(msg,id,ts):
|
|
||||||
print("%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
|
|
||||||
end=None)
|
|
||||||
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
|
|
||||||
for i in msg: print(i,end=None)
|
|
||||||
print()
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
def background():
|
|
||||||
while True:
|
|
||||||
t = logq.get()
|
|
||||||
if not t: break
|
|
||||||
logmsg(*t)
|
|
||||||
|
|
||||||
## ===
|
|
||||||
|
|
||||||
def main():
|
|
||||||
bt = Thread(target=background)
|
|
||||||
bt.start()
|
|
||||||
# This is NOT a good socket location for production, it is for
|
|
||||||
# playing around. I suggest /var/run/milter/myappnamesock for production.
|
|
||||||
socketname = os.path.expanduser('~/pythonsock')
|
|
||||||
timeout = 600
|
|
||||||
# Register to have the Milter factory create instances of your class:
|
|
||||||
Milter.factory = myMilter
|
|
||||||
flags = Milter.CHGBODY + Milter.CHGHDRS + Milter.ADDHDRS
|
|
||||||
flags += Milter.ADDRCPT
|
|
||||||
flags += Milter.DELRCPT
|
|
||||||
Milter.set_flags(flags) # tell Sendmail which features we use
|
|
||||||
print("%s milter startup" % time.strftime('%Y%b%d %H:%M:%S'))
|
|
||||||
sys.stdout.flush()
|
|
||||||
Milter.runmilter("pythonfilter",socketname,timeout)
|
|
||||||
logq.put(None)
|
|
||||||
bt.join()
|
|
||||||
print("%s bms milter shutdown" % time.strftime('%Y%b%d %H:%M:%S'))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# You probably do not need a logging process, but if you do, this
|
|
||||||
# is one way to do it.
|
|
||||||
logq = Queue(maxsize=4)
|
|
||||||
main()
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import testmime
|
|
||||||
import testsample
|
|
||||||
import testutils
|
|
||||||
import testgrey
|
|
||||||
import testcfg
|
|
||||||
import testpolicy
|
|
||||||
import os
|
|
||||||
|
|
||||||
def suite():
|
|
||||||
s = unittest.TestSuite()
|
|
||||||
s.addTest(testmime.suite())
|
|
||||||
s.addTest(testsample.suite())
|
|
||||||
s.addTest(testutils.suite())
|
|
||||||
s.addTest(testgrey.suite())
|
|
||||||
s.addTest(testcfg.suite())
|
|
||||||
s.addTest(testpolicy.suite())
|
|
||||||
return s
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
try: os.remove('test/milter.log')
|
|
||||||
except: pass
|
|
||||||
unittest.TextTestRunner().run(suite())
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
SPF-Pass:example.com OK
|
|
||||||
SPF-Neutral:example.com REJECT
|
|
||||||
HELO-Neutral:example.com OK
|
|
||||||
SPF-Permerror:foo@bad.example.com OK
|
|
||||||
SPF-Permerror: REJECT
|
|
||||||
SMTP-Auth:good@example.com OK
|
|
||||||
SMTP-Auth:example.com REJECT
|
|
||||||
SMTP-Auth:bad@localhost.localdomain REJECT
|
|
||||||
SMTP-Test: REJECT
|
|
||||||
SMTP-Test:.baz.com WILDCARD
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
Received: from www.bmsi.com (bmsweb.bmsi.com [219.109.11.130])
|
|
||||||
by bmsaix.bmsi.com (8.12.1/8.12.1) with ESMTP id g218JVhw028058
|
|
||||||
for <stuart@bmsi.com>; Fri, 1 Mar 2002 03:19:31 -0500
|
|
||||||
Received: from apol ([210.201.89.183])
|
|
||||||
by www.bmsi.com (8.12.1/8.12.1) with SMTP id g218JQkY030600
|
|
||||||
for <stuart@bmsi.com>; Fri, 1 Mar 2002 03:19:27 -0500
|
|
||||||
Date: Fri, 1 Mar 2002 03:19:26 -0500
|
|
||||||
Received: from tcts1
|
|
||||||
by yahoo.com with SMTP id KAqmIGSKwGQHv6LYDEOUUS;
|
|
||||||
Fri, 01 Mar 2002 16:18:13 +0800
|
|
||||||
Message-ID: <VPvce@seed.net.tw>
|
|
||||||
From: 大中華國際留學教育中心@www.bmsi.com
|
|
||||||
To:
|
|
||||||
Subject: 8PxZzvJbH8VtozQ3rC01SOwm =?big5?Q?=A6p=AAG=A7A=B7Q=AFd=BE=C7=AA=BA=B8=DC=A1K?= BwnqwcNylfNuCIM3RG0mCx
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/related;
|
|
||||||
type="multipart/alternative";
|
|
||||||
boundary="----=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJ"
|
|
||||||
X-Mailer: foOkz11rguOMzavzZaDTw
|
|
||||||
X-Priority: 3
|
|
||||||
X-MSMail-Priority: Normal
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
|
|
||||||
------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJ
|
|
||||||
Content-Type: multipart/alternative;
|
|
||||||
boundary="----=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJAA"
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJAA
|
|
||||||
Content-Type: text/html;
|
|
||||||
charset="big5"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
|
|
||||||
PGh0bWwgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiDQp4bWxuczpvPSJ1
|
|
||||||
DQoNCjwvYm9keT4NCg0KPC9odG1sPg==
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJAA--
|
|
||||||
------=_NextPart_kpWBTLcCozjeV8sH5gRbJoOo3aJ--
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
Received: from localhost (localhost)
|
|
||||||
by bmsaix.bmsi.com (8.12.9/8.12.6) id h62JqW5p030912;
|
|
||||||
Wed, 2 Jul 2003 15:52:32 -0400
|
|
||||||
Date: Wed, 2 Jul 2003 15:52:32 -0400
|
|
||||||
From: Mail Delivery Subsystem <MAILER-DAEMON@bmsaix.bmsi.com>
|
|
||||||
Message-Id: <200307021952.h62JqW5p030912@bmsaix.bmsi.com>
|
|
||||||
To: <annagh000@bellsouth.net>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/report; report-type=delivery-status;
|
|
||||||
boundary="h62JqW5p030912.1057175552/bmsaix.bmsi.com"
|
|
||||||
Subject: Returned mail: see transcript for details
|
|
||||||
Auto-Submitted: auto-generated (failure)
|
|
||||||
|
|
||||||
This is a MIME-encapsulated message
|
|
||||||
|
|
||||||
--h62JqW5p030912.1057175552/bmsaix.bmsi.com
|
|
||||||
|
|
||||||
The original message was received at Fri, 27 Jun 2003 15:28:03 -0400
|
|
||||||
from IDENT:ndcHoBWTR9Bf/rEFYJRejRoPTaRDgSCl@bmsweb.bmsi.com [192.168.9.81]
|
|
||||||
|
|
||||||
----- The following addresses had permanent fatal errors -----
|
|
||||||
makurat@erols.com
|
|
||||||
(reason: 452 4.3.0 Filter failure)
|
|
||||||
(expanded from: <makurat@bmsi.com>)
|
|
||||||
|
|
||||||
----- Transcript of session follows -----
|
|
||||||
... while talking to [192.168.9.81]:
|
|
||||||
>>> DATA
|
|
||||||
<<< 452 4.3.0 Filter failure
|
|
||||||
makurat@erols.com... Deferred: 452 4.3.0 Filter failure
|
|
||||||
Message could not be delivered for 5 days
|
|
||||||
Message will be deleted from queue
|
|
||||||
|
|
||||||
--h62JqW5p030912.1057175552/bmsaix.bmsi.com
|
|
||||||
Content-Type: message/delivery-status
|
|
||||||
|
|
||||||
Reporting-MTA: dns; bmsaix.bmsi.com
|
|
||||||
Arrival-Date: Fri, 27 Jun 2003 15:28:03 -0400
|
|
||||||
|
|
||||||
Final-Recipient: RFC822; makurat@bmsi.com
|
|
||||||
X-Actual-Recipient: RFC822; makurat@erols.com
|
|
||||||
Action: failed
|
|
||||||
Status: 4.4.7
|
|
||||||
Remote-MTA: DNS; [192.168.9.81]
|
|
||||||
Diagnostic-Code: SMTP; 452 4.3.0 Filter failure
|
|
||||||
Last-Attempt-Date: Wed, 2 Jul 2003 15:52:32 -0400
|
|
||||||
|
|
||||||
--h62JqW5p030912.1057175552/bmsaix.bmsi.com
|
|
||||||
Content-Type: message/rfc822
|
|
||||||
|
|
||||||
Return-Path: <annagh000@bellsouth.net>
|
|
||||||
Received: from spidey.bmsi.com (IDENT:ndcHoBWTR9Bf/rEFYJRejRoPTaRDgSCl@bmsweb.bmsi.com [192.168.9.81])
|
|
||||||
by bmsaix.bmsi.com (8.12.9/8.12.6) with ESMTP id h5RJS3Vi042394
|
|
||||||
for <makurat@bmsi.com>; Fri, 27 Jun 2003 15:28:03 -0400
|
|
||||||
Received: from sunlong.com ([202.105.130.54])
|
|
||||||
by spidey.bmsi.com (8.11.6/8.11.6) with SMTP id h5RJS2o03547
|
|
||||||
for <makurat@bmsi.com>; Fri, 27 Jun 2003 15:28:02 -0400
|
|
||||||
Message-Id: <200306271928.h5RJS2o03547@spidey.bmsi.com>
|
|
||||||
Received: from mx06.mail.bellsouth.net([218.104.6.10]) by sunlong.com(JetMail 2.5.3.0)
|
|
||||||
with SMTP id jma73efca64b; Fri, 27 Jun 2003 19:23:44 -0000
|
|
||||||
To: <Undisclosed.Recipients@spidey.bmsi.com>
|
|
||||||
From: "Stacy McClain" <annagh000@bellsouth.net>
|
|
||||||
Subject: Defy Gravity in 15 minutes
|
|
||||||
Date: Sat, 28 Jun 2003 03:34:15 -1600
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed;
|
|
||||||
boundary="----=_NextPart_000_646C_00001D33.00000BE1"
|
|
||||||
Reply-To: annagh000@bellsouth.net
|
|
||||||
X-AntiAbuse: : This header was added to track abuse, please include it with any abuse report
|
|
||||||
X-AntiAbuse: Primary Hostname - 210.222.2.13
|
|
||||||
X-Originating-Host: : 210.188.201.159
|
|
||||||
|
|
||||||
------=_NextPart_000_646C_00001D33.00000BE1
|
|
||||||
Content-Type: text/html;
|
|
||||||
charset="iso-8859-1"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
|
|
||||||
PGh0bWw+DQoNCjxoZWFkPg0KPHRpdGxlPjwvdGl0bGU+DQo8L2hlYWQ+DQoNCjxib2R5Pg0KDQo8cD4NCjxhIGhyZWY9Imh0dHA6Ly9zcmQueWFob28uY29tL2Ryc3QvNzQxMjQzMjM1LypodHRwOi93d3cuZnJ5YmVlLmNvbS8iPg0KPGltZyBzcmM9Imh0dHA6Ly8yMTAuMTUuNTEuOTUvcGljX3dlbGwvZ3YyLmdpZiIgYm9yZGVyPSIwIiB3aWR0aD0iNDA1IiBoZWlnaHQ9IjI3MCI+PC9hPjwvcD4NCg0KPHA+DQo8YSBocmVmPSJodHRwOi8vc3JkLnlhaG9vLmNvbS9kcnN0Lzc0MTQxNjg4Mjc3NzcvKmh0dHA6L3d3dy5mcnliZWUuY29tL3BhZ2UvYS5odG1sIj4NCjxpbWcgc3JjPSJodHRwOi8vY2xpY2suanVzdGZvcnlvdS1tYWlsLmNvbS9pbWFnZXMvRjEuZ2lmIiB3aWR0aD0iNDEwIiBoZWlnaHQ9IjE0IiBib3JkZXI9IjAiPjwvYT48L3A+DQoNCjxwIGFsaWduPSJsZWZ0Ij4NCiZuYnNwOzwvcD4NCg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6IDA7IG1hcmdpbi1ib3R0b206IDAiPg0KJm5ic3A7PC9wPg0KDQo8cCBzdHlsZT0ibWFyZ2luLXRvcDogMDsgbWFyZ2luLWJvdHRvbTogMCI+DQombmJzcDs8L3A+DQoNCjxwIHN0eWxlPSJtYXJnaW4tdG9wOiAwOyBtYXJnaW4tYm90dG9tOiAwIj4NCiZuYnNwOzwvcD4NCg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6IDA7IG1hcmdpbi1ib3R0b206IDAiPjxmb250IHNpemU9IjEiPnFhd3NteXp0ciBxYXdzYW9lZHRhZ2ZwdiANCnFhd3N5ZmRhb3FqIHFhd3NjaSBxYXdzY!
|
|
||||||
212Z3ZrIHFhd3NvaW55d3pkbyBxYXdzbXVxYXdza29jIA0KcWF3c2hobmVkZCBxYXdzZWllbiBxYXdzemlnZ3hucGN2cyBxYXdzd3lkZSBxYXdzeWFwIHFhd3NxamVkeWhxYXdzZmt1bSANCnFhd3NmbSBxYXdzdW11Ym1mYmR3IHFhd3Nkc29ka2xvIHFhd3Nhc2VtayBxYXdzZXdzIHFhd3NxdWRneGVvcWF3c3J6IA0KcWF3c290dSBxYXdzcHplbnJoZW1xYSBxYXdzdXplcmpqcWZxIHFhd3NydWFucyBxYXdzbnBjcGFoZ2pwIHFhd3NxYXdoZHJxYXdzYmFscXNxaiANCnFhd3N5bmggcWF3c2VrIHFhd3N0YmNndGd0IHFhd3N0ZnhzeHd4ICBxYXdzandlcHFhd3NsYmN6ZWRuIHFhd3NzcW1nb3YgDQpxYXdzZ3phdiBxYXdzZ2N2aCBxYXdzd21sYWt1bW5sbiBxYXdzZHpqcW9yeCBxYXdzdGhvbHRmaWxmeHFhd3NpcGJneSANCnFhd3NpbHp5Znd2dnMgIHFhd3NpdmJwdmNiIHFhd3NrZXRpYmtocGRhIHFhd3N6ZmJqYm1yayBxYXdzbWZvZ29ucWF3c2FvIA0KcWF3c21vcXggcWF3c3FkeWVuaCBxYXdzYnMgcWF3c2l5aXBkYWx4IHFhd3N6aXlpbyBxYXdzaWZ6dXFyamltcSANCnFhd3NuayBxYXdza3dhciBxYXdzanNleHNmc2IgcWF3c3RxaWlhY2cgcWF3c2p0YnFobnFlIHFhd3Niam1pcGpxYXdzaHl4anNwbXhuIA0KIHFhd3NqcmJlbnIgcWF3c3p6b3p0ZndydyBxYXdzZ25uaHdjIHFhd3NrdXkgcWF3c3ZwcWF3c25qbmd5eHl1eCBxYXdzd3lvc2EgDQpxYXdzb2lnIHFhd3Nub25rcm5pbWcgcWF3c2NtcGdxemtwcm!
|
|
||||||
U8L2ZvbnQ+PC9wPg0KDQo8L2JvZHk+DQoNCjwvaHRtbD48L3RpdGxlPg0K
|
|
||||||
|
|
||||||
------=_NextPart_000_646C_00001D33.00000BE1--
|
|
||||||
|
|
||||||
|
|
||||||
--h62JqW5p030912.1057175552/bmsaix.bmsi.com--
|
|
||||||
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
Received: from zuul.kastle.com (root@localhost)
|
|
||||||
by zuul.kastle.com with ESMTP id h7JGdwn27534
|
|
||||||
for <amy@koger.bmsi.com>; Tue, 19 Aug 2003 12:39:58 -0400 (EDT)
|
|
||||||
Received: from kastle.com (netgate.kastle.com [172.17.2.8])
|
|
||||||
by zuul.kastle.com with ESMTP id h7JGdwV27530
|
|
||||||
for <amy@koger.bmsi.com>; Tue, 19 Aug 2003 12:39:58 -0400 (EDT)
|
|
||||||
Received: by kastle.com
|
|
||||||
with XWall v3.27 ;
|
|
||||||
Tue, 19 Aug 2003 12:45:41 -0400
|
|
||||||
From: System Administrator <postmaster@kastle.com>
|
|
||||||
To: "amy@koger.bmsi.com" <amy@koger.bmsi.com>
|
|
||||||
Subject: Non delivery report: 5.9.5 (Blocked attachment)
|
|
||||||
Date: Tue, 19 Aug 2003 12:45:41 -0400
|
|
||||||
X-Mailer: XWall v3.27
|
|
||||||
Mime-Version: 1.0
|
|
||||||
Content-Type: multipart/report; report-type=delivery-status;
|
|
||||||
boundary="_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb"
|
|
||||||
|
|
||||||
This is a multi part message in MIME format.
|
|
||||||
|
|
||||||
--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb
|
|
||||||
Content-Type: text/plain; charset="us-ascii"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Your message
|
|
||||||
|
|
||||||
From: amy@koger.bmsi.com
|
|
||||||
|
|
||||||
To: lwilliams@kastle.com
|
|
||||||
|
|
||||||
Subj: Thank you!
|
|
||||||
Sent: 2003-08-19 08:51
|
|
||||||
|
|
||||||
has encountered a delivery problem.
|
|
||||||
|
|
||||||
|
|
||||||
Reason: Blocked attachment
|
|
||||||
One of the attachment(s) in the message is blocked.
|
|
||||||
For security reasons the message was not or not completely delivered to
|
|
||||||
the recipient.
|
|
||||||
|
|
||||||
Additional info:
|
|
||||||
The blocked attachment is: thank_you.pif
|
|
||||||
|
|
||||||
--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb
|
|
||||||
Content-Type: message/xdelivery-status ; name="delivery-status.txt"
|
|
||||||
|
|
||||||
Reporting-MTA: dns; kastle.com
|
|
||||||
Received-From-MTA: dns; zuul.kastle.com
|
|
||||||
Arrival-Date: Tue, 19 Aug 2003 12:45:41 -0400
|
|
||||||
|
|
||||||
Final-Recipient: rfc822; lwilliams@kastle.com
|
|
||||||
Action: failed
|
|
||||||
Status: 5.9.5
|
|
||||||
|
|
||||||
--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb
|
|
||||||
Content-Type: message/rfc822
|
|
||||||
|
|
||||||
Received: from zuul.kastle.com [172.17.2.100]
|
|
||||||
by kastle.com
|
|
||||||
with XWall v3.27 ;
|
|
||||||
Tue, 19 Aug 2003 12:45:41 -0400
|
|
||||||
Received: from zuul.kastle.com (root@localhost)
|
|
||||||
by zuul.kastle.com with ESMTP id h7JGduo27526
|
|
||||||
for <lwilliams@kastle.com>; Tue, 19 Aug 2003 12:39:56 -0400 (EDT)
|
|
||||||
Received: from 1333AVE2 (wan-vc8f35e.norva3.biz.mindspring.com [216.135.140.174])
|
|
||||||
by zuul.kastle.com with ESMTP id h7JGdqS27522
|
|
||||||
for <lwilliams@kastle.com>; Tue, 19 Aug 2003 12:39:53 -0400 (EDT)
|
|
||||||
Message-Id: <200308191639.h7JGdqS27522@zuul.kastle.com>
|
|
||||||
From: <amy@koger.bmsi.com>
|
|
||||||
To: <lwilliams@kastle.com>
|
|
||||||
Subject: Thank you!
|
|
||||||
Date: Tue, 19 Aug 2003 12:51:38 --0400
|
|
||||||
X-MailScanner: Found to be clean
|
|
||||||
Importance: Normal
|
|
||||||
X-Mailer: Microsoft Outlook Express 6.00.2600.0000
|
|
||||||
X-MSMail-Priority: Normal
|
|
||||||
X-Priority: 3 (Normal)
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed;
|
|
||||||
boundary="_NextPart_000_062C48F7"
|
|
||||||
|
|
||||||
--_NextPart_1_qmZrHLajoetbkwlTZTViemHPfyb--
|
|
||||||
|
|
||||||
|
|
||||||
-84
@@ -1,84 +0,0 @@
|
|||||||
From dspam Mon Sep 29 16:36:23 2003
|
|
||||||
Received: from orcon.net.nz (port-219-88-129-82.orcon.net.nz [219.88.129.82])
|
|
||||||
by spidey.planet.com (8.11.6/8.11.6) with SMTP id h8Q85c414321
|
|
||||||
for <postmaster@bugle.com>; Fri, 26 Sep 2003 04:05:39 -0400
|
|
||||||
Date: Fri, 26 Sep 2003 20:05:56 +1200
|
|
||||||
From: Mail Delivery Subsystem <MAILER-DAEMON@orcon.net.nz>
|
|
||||||
Message-Id: <200309262005.IEI23104@mx1.orcon.net.nz>
|
|
||||||
To: <postmaster@bugle.com>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/report; report-type=delivery-status;
|
|
||||||
boundary="IEI23104.1064534400/mx1.orcon.net.nz"
|
|
||||||
Subject: Returned mail: User unknown
|
|
||||||
Auto-Submitted: auto-generated (failure)
|
|
||||||
X-DSpam-HeaderScore: 0.007433
|
|
||||||
|
|
||||||
This is a MIME-encapsulated message
|
|
||||||
|
|
||||||
--IEI23104.1064534400/mx1.orcon.net.nz
|
|
||||||
|
|
||||||
The original message was received at Fri, 26 Sep 2003 20:05:56 +1200
|
|
||||||
from
|
|
||||||
|
|
||||||
----- The following addresses had permanent fatal errors -----
|
|
||||||
<mike-liz@orcon.net.nz>
|
|
||||||
(expanded from: <mike-liz@orcon.net.nz>)
|
|
||||||
|
|
||||||
----- Transcript of session follows -----
|
|
||||||
mail.local: unknown name: mike-liz
|
|
||||||
550 <mike-liz@orcon.net.nz>... User unknown
|
|
||||||
|
|
||||||
--IEI23104.1064534400/mx1.orcon.net.nz
|
|
||||||
Content-Type: message/delivery-status
|
|
||||||
|
|
||||||
Reporting-MTA: dns; mx1.orcon.net.nz
|
|
||||||
Received-From-MTA: DNS;
|
|
||||||
Arrival-Date: Fri, 26 Sep 2003 20:05:56 +1200
|
|
||||||
|
|
||||||
Final-Recipient: RFC822; <mike-liz@orcon.net.nz>
|
|
||||||
X-Actual-Recipient: RFC822; mike-liz@orcon.net.nz
|
|
||||||
Action: failed
|
|
||||||
Status: 5.1.1
|
|
||||||
Last-Attempt-Date: Fri, 26 Sep 2003 20:05:56 +1200
|
|
||||||
|
|
||||||
--IEI23104.1064534400/mx1.orcon.net.nz
|
|
||||||
Content-Type: message/rfc822
|
|
||||||
|
|
||||||
Return-Path: <MAILER-DAEMON>
|
|
||||||
Received: from global_1.bugle.com ([12.4.120.82])
|
|
||||||
by dbmail-mx3.orcon.co.nz (8.12.6/8.12.6/Debian-7) with ESMTP id h8O6CRJ8015038
|
|
||||||
for <mike-liz@orcon.net.nz>; Wed, 24 Sep 2003 18:12:28 +1200
|
|
||||||
From: postmaster@bugle.com
|
|
||||||
To: mike-liz@orcon.net.nz
|
|
||||||
Date: Wed, 24 Sep 2003 02:13:53 -0400
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/report; report-type=delivery-status;
|
|
||||||
boundary="9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle."
|
|
||||||
X-DSNContext: 335a7efd - 4457 - 00000001 - 80040546
|
|
||||||
Message-ID: <YkMSnhRpy00001453@global_1.bugle.com>
|
|
||||||
Subject: Delivery Status Notification (Failure)
|
|
||||||
X-Spam-Score: 3.5 (***) BANG_MONEY,CASHCASHCASH,EXCUSE_10,EXCUSE_14,MAILTO_TO_SPAM_ADDR,NO_REAL_NAME,SENT_IN_COMPLIANCE
|
|
||||||
X-Scanned-By: MIMEDefang 2.32 (www . roaringpenguin . com / mimedefang)
|
|
||||||
This is a MIME-formatted message.
|
|
||||||
Portions of this message may be unreadable without a MIME-capable mail program.
|
|
||||||
|
|
||||||
--9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle.
|
|
||||||
Content-Type: text/plain; charset=unicode-1-1-utf-7
|
|
||||||
|
|
||||||
This is an automatically generated Delivery Status Notification.
|
|
||||||
|
|
||||||
Delivery to the following recipients failed.
|
|
||||||
|
|
||||||
jholt@bugle.com
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--9B095B5ADSN=_01C3664F7D2C23400000BC00global_1.bugle.
|
|
||||||
Content-Type: message/delivery-status
|
|
||||||
|
|
||||||
Reporting-MTA: dns;global_1.bugle.com
|
|
||||||
Received-From-MTA: dns;gts.bugle.com
|
|
||||||
--IEI23104.1064534400/mx1.orcon.net.nz--
|
|
||||||
|
|
||||||
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
From: downs <downs@elit.com>
|
|
||||||
To: luv@elit.com
|
|
||||||
Subject: Hello,luv,welcome to my hometown
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/alternative;
|
|
||||||
boundary=Rer34xd7vC5E6b434MS3soP671RCD8
|
|
||||||
|
|
||||||
--Rer34xd7vC5E6b434MS3soP671RCD8
|
|
||||||
Content-Type: text/html;
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
<HTML><HEAD></HEAD><BODY>
|
|
||||||
<iframe src=3Dcid:Q2Xet76Sg02 height=3D0 width=3D0>
|
|
||||||
</iframe>
|
|
||||||
<FONT></FONT></BODY></HTML>
|
|
||||||
|
|
||||||
--Rer34xd7vC5E6b434MS3soP671RCD8
|
|
||||||
Content-Type: audio/x-wav;
|
|
||||||
name=story[1].scr
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-ID: <Q2Xet76Sg02>
|
|
||||||
|
|
||||||
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
||||||
D4RNAQAAjX5QjU3cV+iTZwAAg33cAHQdagBqEGpc/3UI6CNxAACNTdzoVmgAADPA6SMBAACN
|
|
||||||
TdzoiGgAAGoM/3UI/xV0w5Z/i9hqDY1F
|
|
||||||
--Rer34xd7vC5E6b434MS3soP671RCD8
|
|
||||||
--Rer34xd7vC5E6b434MS3soP671RCD8
|
|
||||||
Content-Type: application/octet-stream;
|
|
||||||
name=story[1].asp
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-ID: <Q2Xet76Sg02>
|
|
||||||
|
|
||||||
H4sIAAAAAAAAA8Uca3ObSPJzXJX/0MttxU6t9bYdO7G0hxG22Oi1gOzz1VWlRmgksUagBWTF
|
|
||||||
6DZXKrcVuTeUWdAlKkRVJNmTg42MD2OJHsZjeLgZpcNEs95+ECFOEhecV9jffuEP7I+h4cP/
|
|
||||||
AMwafOuETQAA
|
|
||||||
--Rer34xd7vC5E6b434MS3soP671RCD8--
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
From leec@windowsshop.com Fri Sep 10 11:48:25 2004
|
|
||||||
Message-ID: <4141CDD4.7040305@windowsshop.com>
|
|
||||||
Date: Fri, 10 Sep 2004 11:52:52 -0400
|
|
||||||
From: Lee Connor <leec@windowsshop.com>
|
|
||||||
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.4) Gecko/20030624 Netscape/7.1 (ax)
|
|
||||||
X-Accept-Language: en-us, en
|
|
||||||
MIME-Version: 1.0
|
|
||||||
To: Cleo Matthews-Conley <cleom@windowsshop.com>,
|
|
||||||
Tony Collini <tonyc@windowsshop.com>,
|
|
||||||
John Higinbothom <johnh@windowsshop.com>
|
|
||||||
CC: Rich Higgins <richh@windowsshop.com>
|
|
||||||
Subject: [Fwd: [Fwd: Customer Concerns]]
|
|
||||||
Content-Type: multipart/mixed;
|
|
||||||
boundary="------------020209070802060007090105"
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
--------------020209070802060007090105
|
|
||||||
Content-Type: text/plain; charset=us-ascii; format=flowed
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Cleo - please review attached feedback from Sales team.......I recall at
|
|
||||||
an early meeting after we moved in you and Tony (and maybe 1 or 2
|
|
||||||
others) were going to develop a voice mail procedure or instruction
|
|
||||||
sheet for all staff. It looks like we really need this to get what we
|
|
||||||
are looking for from the system. Please let me know when you can produce
|
|
||||||
this and give a draft to the managers here for review.
|
|
||||||
Thanks,
|
|
||||||
Lee
|
|
||||||
|
|
||||||
|
|
||||||
--------------020209070802060007090105
|
|
||||||
Content-Type: message/rfc822;
|
|
||||||
name="[Fwd: Customer Concerns]"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
Content-Disposition: inline;
|
|
||||||
filename="[Fwd: Customer Concerns]"
|
|
||||||
|
|
||||||
Return-Path: <richh@windowsshop.com>
|
|
||||||
Received: from windowsshop.com (pc147.windowsshop.com [192.168.100.147] (may be forged))
|
|
||||||
by lord.windowsshop.com (8.12.10/8.12.10) with ESMTP id i89KCClX003425
|
|
||||||
for <leec@windowsshop.com>; Thu, 9 Sep 2004 16:12:12 -0400
|
|
||||||
Message-ID: <4140B851.3020501@windowsshop.com>
|
|
||||||
Date: Thu, 09 Sep 2004 16:08:49 -0400
|
|
||||||
From: Rich <richh@windowsshop.com>
|
|
||||||
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.0.2) Gecko/20021120 Netscape/7.01
|
|
||||||
X-Accept-Language: en-us, en
|
|
||||||
MIME-Version: 1.0
|
|
||||||
To: Lee Connor <leec@windowsshop.com>
|
|
||||||
Subject: [Fwd: Customer Concerns]
|
|
||||||
Content-Type: multipart/mixed;
|
|
||||||
boundary="------------030301030706020401010801"
|
|
||||||
X-DSpam-Score: 0.000000
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
--------------030301030706020401010801
|
|
||||||
Content-Type: text/plain; charset=us-ascii; format=flowed
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Lee - do you want me to do anything else with this?
|
|
||||||
|
|
||||||
Rich
|
|
||||||
|
|
||||||
<!DSPAM:FEE4D3278234264874834386>
|
|
||||||
|
|
||||||
|
|
||||||
--------------030301030706020401010801
|
|
||||||
Content-Type: message/rfc822; name="Customer Concerns";
|
|
||||||
boundary="===============0045392615=="
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
Content-Disposition: inline;
|
|
||||||
filename="Customer Concerns"
|
|
||||||
|
|
||||||
|
|
||||||
Return-Path: <joes@windowsshop.com>
|
|
||||||
Received: from joes (pc148.windowsshop.com [192.168.100.148] (may be forged))
|
|
||||||
by lord.windowsshop.com (8.12.10/8.12.10) with SMTP id i89K9BlX003262
|
|
||||||
for <richh@windowsshop.com>; Thu, 9 Sep 2004 16:09:11 -0400
|
|
||||||
From: "Joe Schmuck" <joes@windowsshop.com>
|
|
||||||
To: <richh@windowsshop.com>
|
|
||||||
Subject: Customer Concerns
|
|
||||||
Date: Thu, 9 Sep 2004 16:08:26 -0400
|
|
||||||
Message-ID: <OFEPKHCCLPIECLFBLDHBAEAECAAA.joes@windowsshop.com>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: text/plain;
|
|
||||||
charset="iso-8859-1"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
X-Priority: 3 (Normal)
|
|
||||||
X-MSMail-Priority: Normal
|
|
||||||
X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2910.0)
|
|
||||||
Importance: Normal
|
|
||||||
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1106
|
|
||||||
X-DSpam-Score: 0.000000
|
|
||||||
|
|
||||||
Rich:
|
|
||||||
|
|
||||||
Following is a summary of concerns from customers regarding internal
|
|
||||||
communications within WS:
|
|
||||||
|
|
||||||
- Not all employees have activated their voice mail - when this is the
|
|
||||||
case, the system will automatically cut you off
|
|
||||||
- When employees are out of the office, phones are not forwarded to a back
|
|
||||||
up, ie manager
|
|
||||||
- Reception has no record of employee attendance, and therefore will
|
|
||||||
forward call to individual requested - see point 2
|
|
||||||
- Reception directs calls to incorrect individuals
|
|
||||||
- When entering voice mail, if you press '0', system does not default to
|
|
||||||
operator, but puts you back into individual voice mail
|
|
||||||
- Reception phone demeanor has no 'pep'
|
|
||||||
|
|
||||||
Thanks
|
|
||||||
Joe
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
Outgoing mail is certified Virus Free.
|
|
||||||
Checked by AVG anti-virus system (http://www.grisoft.com).
|
|
||||||
Version: 6.0.752 / Virus Database: 503 - Release Date: 9/3/2004
|
|
||||||
|
|
||||||
|
|
||||||
<!DSPAM:FEE4D05F1332634871908793>
|
|
||||||
|
|
||||||
--===============0045392615==--
|
|
||||||
--------------030301030706020401010801--
|
|
||||||
|
|
||||||
--------------020209070802060007090105--
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# sample SRS configuration
|
|
||||||
[srs]
|
|
||||||
;secret="shhhh!"
|
|
||||||
;maxage=21
|
|
||||||
;hashlength=5
|
|
||||||
# if defined, SRS uses a database for opaque rewriting
|
|
||||||
;database=/var/log/milter/srsdata
|
|
||||||
# sign these domains using SES to prevent forged bounces instead of SRS
|
|
||||||
;ses = localdomain1.com, localdomain2.org
|
|
||||||
# sign these domains using SRS in signing mode to prevent forged bounces
|
|
||||||
;sign = localdomain1.com, localdomain2.org
|
|
||||||
# rewrite all other domains to this domain using SRS
|
|
||||||
;fwdomain = mydomain.com
|
|
||||||
# additional domains to decode (reverse) srs
|
|
||||||
# NOTE: bms.py in milter package can also do this, as can pysrs.m4 HACK.
|
|
||||||
;srs = otherdomain.com
|
|
||||||
# do not rewrite mail to these domains
|
|
||||||
;nosrs = braindeadmail.com
|
|
||||||
# Treat these localparts as a DSN. Lot's of braindead systems
|
|
||||||
# send non-DSN mail to MAIL FROM.
|
|
||||||
;banned_users = mailer-daemon, clamav, postmaster
|
|
||||||
|
|
||||||
[srsmilter]
|
|
||||||
;datadir=/var/lib/milter
|
|
||||||
socketname = /var/run/milter/srsmilter
|
|
||||||
miltername = pysrsfilter
|
|
||||||
# reject DSNs to unsigned recipients (bounce spam)
|
|
||||||
reject_spoofed = true
|
|
||||||
;trusted_relay = 1.2.3.4
|
|
||||||
internal_connect = 192.168.*.*,127.0.0.1,::1
|
|
||||||
# Enable outgoing SRS via CHGFROM (see code for limitations)
|
|
||||||
miltersrs = false
|
|
||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
Return-Path: <lauren@foobar.com>
|
|
||||||
Received: from foobar.com (localhost [127.0.0.1])
|
|
||||||
by hemholt.foobar.com (8.9.3/8.8.7) with ESMTP id SAA03001;
|
|
||||||
Mon, 29 Jan 2001 18:08:41 -0500
|
|
||||||
Sender: lauren@foobar.com
|
|
||||||
Message-ID: <3A75F7F6.CBF9E75@foobar.com>
|
|
||||||
Date: Mon, 29 Jan 2001 18:08:39 -0500
|
|
||||||
From: Lauren Hemholz <lauren@foobar.com>
|
|
||||||
Organization: Hemholtz Family
|
|
||||||
X-Mailer: Mozilla 4.76 [en] (X11; U; Linux 2.2.16-3 i586)
|
|
||||||
X-Accept-Language: en
|
|
||||||
MIME-Version: 1.0
|
|
||||||
To: Jriser13@aol.com
|
|
||||||
Subject: Re: P.B.S kids
|
|
||||||
References: <e4.1045e74c.27a7018b@aol.com>
|
|
||||||
Content-Type: multipart/alternative;
|
|
||||||
boundary="------------7EC2082FC4F651D73FCD6FE1"
|
|
||||||
Status: O
|
|
||||||
|
|
||||||
|
|
||||||
--------------7EC2082FC4F651D73FCD6FE1
|
|
||||||
Content-Type: text/plain; charset=us-ascii
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Dear Agent 1
|
|
||||||
I hope you can read this. Whenever you write label it P.B.S kids.
|
|
||||||
Eliza doesn't know a thing about P.B.S kids. got to go by
|
|
||||||
agent one.
|
|
||||||
|
|
||||||
--------------7EC2082FC4F651D73FCD6FE1
|
|
||||||
Content-Type: text/html; charset=us-ascii
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
<!doctype html public "-//w3c//dtd html 4.0 transitional//en">
|
|
||||||
<html>
|
|
||||||
<font color="#FFCCCC">Dear Agent 1</font>
|
|
||||||
<br><font color="#66FFFF">I hope you can read this. </font><font color="#FFCC33">Whenever
|
|
||||||
you write label it </font><font color="#993399">P.</font><font color="#000000">B.</font><font color="#66FFFF">S
|
|
||||||
</font><font color="#3366FF">kids.</font>
|
|
||||||
<br><font color="#3366FF"> Eliza doesn't know a thing about
|
|
||||||
</font><font color="#993399">P.</font><font color="#000000">B.</font><font color="#66FFFF">S
|
|
||||||
</font><font color="#3366FF">kids. got to go by</font>
|
|
||||||
<br>agent one.</html>
|
|
||||||
|
|
||||||
--------------7EC2082FC4F651D73FCD6FE1--
|
|
||||||
|
|
||||||
-497
@@ -1,497 +0,0 @@
|
|||||||
Received: from smtp01.mrf.mail.rcn.net (smtp01.mrf.mail.rcn.net [207.172.4.60])
|
|
||||||
by www.bmsi.com (8.12.1/8.12.1) with ESMTP id g42A1XGQ014740
|
|
||||||
for <makurat@bmsi.com>; Thu, 2 May 2002 06:01:33 -0400
|
|
||||||
Received: from 66-44-42-109.s617.apx1.lnhdc.md.dialup.rcn.com ([66.44.42.109] helo=fjoneill)
|
|
||||||
by smtp01.mrf.mail.rcn.net with smtp (Exim 3.33 #10)
|
|
||||||
id 173DOu-0004vQ-00; Thu, 02 May 2002 06:01:26 -0400
|
|
||||||
From: "Francis J. O'Neill" <fjoneill@erols.com>
|
|
||||||
To: "Atkinson, Steve" <scatkinson@ieee.org>,
|
|
||||||
"Blewett, John" <sixpackdad@aol.com>,
|
|
||||||
"Carroll, Matt & Jane" <janematt@ix.netcom.com>,
|
|
||||||
"Donovan, Kathleen" <rspatcelbr@aol.com>,
|
|
||||||
"Fitzpatrick, Vince" <vfitzpatrick@rjagroup.com>,
|
|
||||||
"Flannery, Jessica & Beth" <jeanmflan@aol.com>,
|
|
||||||
"Fontaine, Gene" <fontaineg@hotmail.com>, "Fox, Bob" <wvfoxmanva@aol.com>,
|
|
||||||
"Gerken, K." <kevin_gerken@faa.gov>,
|
|
||||||
"Gerken, Kevin \(Home\)" <gerken@msn.com>,
|
|
||||||
"Hagan, Carl & Jan" <hagan9600@aol.com>,
|
|
||||||
"Hardcastle, Joe & Carol" <ch4avon@aol.com>,
|
|
||||||
"Hardcastle, Joe" <jhardc8400@aol.com>,
|
|
||||||
"Hendrickson, Scott" <umuc_scott@yahoo.com>,
|
|
||||||
"Holl, Mike" <matbxholl@aol.com>,
|
|
||||||
"Jaworski, Francis J" <sevengatefarm@aol.com>, "JC" <jgcannon@aol.com>,
|
|
||||||
"Joe & Kathy Martin" <joe.martin@focuspoint.com>,
|
|
||||||
"Joe & Kathy Martin" <kjams5@aol.com>, "Kendle, Greg" <gkendle@erols.com>,
|
|
||||||
<Moptop1998@aol.com>, "pquell" <pquell@aol.com>,
|
|
||||||
"Quinan, Phil" <philq@fgm.com>, "Quintana, G" <glquintana@aol.com>,
|
|
||||||
"Rannazzisi, Jim" <jimrazz@aol.com>, "Reed, Kathi" <Mrsreedyreed@aol.com>,
|
|
||||||
"Serini, Pete" <serinip1@aol.com>, "Sherry, Ed" <gses56@earthlink.net>,
|
|
||||||
"Smith, T.J." <tsmit40@aol.com>,
|
|
||||||
"Southard, Jack & Ann" <Jacksout111@cs.com>,
|
|
||||||
"Terza, Rick" <srterza@hotmail.com>, "White, Diane" <dwhite703@aol.com>,
|
|
||||||
"Tisdale, David" <djtisdale@earthlink.net>,
|
|
||||||
"Zilka, Skip & Adella Mae" <skp406@cs.com>,
|
|
||||||
"Worrick, Matt & Dyanne" <worrickgrim@comcast.net>,
|
|
||||||
"Worrick, Matt" <worrickm@nima.mil>,
|
|
||||||
"Weaver Bob & Carol" <Bingobobby@aol.com>,
|
|
||||||
"Villa, Al & Jennifer" <avilla@sysplan.com>,
|
|
||||||
"Van Doren, Frank & Joan" <jfvandoren@aol.com>,
|
|
||||||
"Trudeau, Tom & Jeri" <trudeau7369@yahoo.com>,
|
|
||||||
"Trowbridge, Paul" <paltrow@starpower.net>,
|
|
||||||
"Trotter, Robert R." <robiedo@juno.com>,
|
|
||||||
"Tracy, Mike & Patty" <ptracy161@comcast.net>,
|
|
||||||
"Tonnessen, Jim & Maria" <Bosn1@aol.com>,
|
|
||||||
"Templeton, Pat" <pat.templeton@bcinow.com>,
|
|
||||||
"Taylor, Michelle" <Michelle_Taylor@datatel.com>,
|
|
||||||
"Taylor, Fran & Janet" <FranJanMom@aol.com>,
|
|
||||||
"Summit, Adelaide" <adandarn@aol.com>,
|
|
||||||
"Stalker, Nicole" <jmstalker@comcast.net>,
|
|
||||||
"Snidal, Brian" <sniwal@yahoo.com>, "Smith Danielle" <tsmith7746@aol.com>,
|
|
||||||
"Shorten, Jim & Marcia" <shor10@juno.com>,
|
|
||||||
"Scoffone, Dave" <dscoff1@hotmail.com>,
|
|
||||||
"Ryder, Tom & Kim" <threeryders@erols.com>,
|
|
||||||
"Ryder, Larry & Kate" <lkaryder@aol.com>, "Rossi, Ralph" <rr2520@aol.com>,
|
|
||||||
"Ross, Scott" <knighted@msn.com>, "Riley, Francis" <fxrdem@aol.com>,
|
|
||||||
"Riley, Dave & Susan" <mtatlas@aol.com>,
|
|
||||||
"Riley Tom & Marie" <triley0574@comcast.net>,
|
|
||||||
"Reynolds, Tommy" <treynolds009@hotmail.com>,
|
|
||||||
"Reynolds, Jim & Noreen" <reynolds-tribe@msn.com>,
|
|
||||||
"Quintana Dick" <richard.p.quintana@cpmx.mail.saic.com>,
|
|
||||||
"Purdy, Larry & Anne" <Purdy7@juno.com>, "Post, Harold" <hpost@vt.edu>,
|
|
||||||
"Podledsak, Tom" <tpodlesak@arl.mil>,
|
|
||||||
"Pino, Ernie & Gloria" <emanpino@juno.com>,
|
|
||||||
"Pasieka, Tony & Katy" <Pasiekat@yahoo.com>,
|
|
||||||
"Partsch, Jerry & Monica" <gpartsch@comcast.net>,
|
|
||||||
"Ong, Ken" <kennethong78@hotmail.com>, "O'Neill, Mike" <moneill@gmu.edu>,
|
|
||||||
"O'Neill, Frank" <fjoneill@erols.com>,
|
|
||||||
"Oliver, John & Juanita" <jmloliver@aol.com>,
|
|
||||||
"O'Hanlon, Peter \(Work\)" <pohanlon@uspsoig.gov>,
|
|
||||||
"O'Hanlon Peter & Anne" <aohanlon@manassas.k12.va.us>,
|
|
||||||
"Noonan, Tim & Bettie" <bbnoonan@juno.com>,
|
|
||||||
"Newton Bill" <golfnbill15@msn.com>, "Nannery, Phil" <pnannery@tla.com>,
|
|
||||||
"Nannery, Alison" <alisonnannery@yahoo.com>,
|
|
||||||
"Myrum, Marc" <myrumma@hotmail.com>,
|
|
||||||
"Murphy, John & Karen" <JCM2nd@msn.com>,
|
|
||||||
"Mullen,OSB, Father Godfrey" <GodfreyOSB@erols.com>,
|
|
||||||
"McCusker, JP & Maggie" <mccusker@af.pentagon.mil>,
|
|
||||||
"McCusker, J.P. & Maggie" <jpandmaggie@aol.com>,
|
|
||||||
"Mathers, David & Kathy" <davidandkathy@compuserve.com>,
|
|
||||||
"Makurat, Dennis" <makurat@bmsi.com>,
|
|
||||||
"Lord, Kevin & Gail" <Lordhaus@netzero.net>,
|
|
||||||
"Linehan, Pat" <prpjtdkl@aol.com>, "Linehan, Kellie" <kekalee427@aol.com>,
|
|
||||||
"linehan, Joe" <cadetbrat@aol.com>,
|
|
||||||
"Lewandowski, Matt & Mary" <matt@chipware.com>,
|
|
||||||
"Lester Doug" <Lester_doug@bah.com>, "Kurz, Al & Sandra" <ARKurz@erols.com>,
|
|
||||||
"Koeppel Bruce & Carolyn" <Koeppelb@oceusa.com>,
|
|
||||||
"Kindergan Bob & Dee" <bka2@att.net>,
|
|
||||||
"Kerzner, Ken & Maureen" <auzguyz1@comcast.net>,
|
|
||||||
"Keating, Russ & Julexy" <russty@juno.com>,
|
|
||||||
"Johnson, Laura" <davidjohnsonrealtor@yahoo.com>,
|
|
||||||
"Johns, Milt & Shellie" <miltesq@aol.com>,
|
|
||||||
"Jacobeen, Dave & Maria" <jacobeen@erols.com>,
|
|
||||||
"Hilchey, Paul" <paulhilchey@juno.com>,
|
|
||||||
"Head, Rich & Judy" <rghead@aol.com>,
|
|
||||||
"Hart Bob & Lorraine" <hartstv@aol.com>,
|
|
||||||
"Harrington, Thom" <t.j.harrington@ieee.org>,
|
|
||||||
"Harrington Cathy" <cathyH@atcc.org>,
|
|
||||||
"Hammersley, Ron & Ladavadee" <RHammer849@aol.com>,
|
|
||||||
"Grimes, Li nda & Frank" <lnf67@erols.com>,
|
|
||||||
"Gregory, Glen" <ikhnaton@geek.com>,
|
|
||||||
"Gregory Bob & Peggy" <pegory1@netzero.net>,
|
|
||||||
"Greco, Joe & Ann" <jgreco104@aol.com>,
|
|
||||||
"Goodman, Bill & Marcia" <bmgoodman@aol.com>,
|
|
||||||
"Goble, Theresa" <tagoman@juno.com>,
|
|
||||||
"Goble Dick & Theresa" <tagoman@aol.com>,
|
|
||||||
"Glennon John" <John.Glennon@fepoc.carefirst.com>,
|
|
||||||
"Gendron, Ray & Barbara" <gendronb1@erols.com>,
|
|
||||||
"Gendron, Jerry" <jbgendron@webtv.net>,
|
|
||||||
"Gaynord, Bill & Linda" <lbgaynord@aol.com>,
|
|
||||||
"Gareis Charlie" <gareiscj@aol.com>,
|
|
||||||
"Gagat, Ron & Judy" <RGagat6314@aol.com>,
|
|
||||||
"Ford, Bobby & Mauren" <bobf@erols.com>,
|
|
||||||
"Fontaine, George & Jo" <fontneg@comcast.net>,
|
|
||||||
"Flannery Bill" <wflannery@anteon.com>, "Fini Bob & Beth" <rfini@erols.com>,
|
|
||||||
"Ferraro, Sonia & Jack" <soniaferraro@earthlink.com>,
|
|
||||||
"Ferraro, Jack & Sonia" <jpferraro@earthlink.net>,
|
|
||||||
"Farquhar Butch & Rosa" <afarquhar8@comcast.net>,
|
|
||||||
"Egitto, John & Ann" <egittos@yahoo.com>,
|
|
||||||
"Economou, Tina" <annenick@erols.com>,
|
|
||||||
"Drummond, Scott" <drummond.scott@verizon.net>,
|
|
||||||
"Drummond, Cheryl" <cheryl.drummond@verizon.net>,
|
|
||||||
"Dennin Bob & Mary Jane" <rdennin@aol.com>,
|
|
||||||
"Daudet, Darryl & Jean" <dkdaudet@aol.com>,
|
|
||||||
"Dale Charles" <Cdale@erols.com>,
|
|
||||||
"Conde, Norman & Josephine" <nconde@comcast.net>,
|
|
||||||
"Colgan, Charles" <charlescolgan@colganair.com>,
|
|
||||||
"Clarke Russ & Pat" <clarkert@comcast.net>,
|
|
||||||
"Charters, Nikki" <fitzfam@starpower.net>,
|
|
||||||
"Carta, Mike & Sallie" <mcrt8@cs.com>,
|
|
||||||
"Carroll, Pat & Debbie" <dpcarroll981@aol.com>,
|
|
||||||
"Capozoli, Tom" <GoogCapo@aol.com>, "Capozoli, Patty" <pbcapo@aol.com>,
|
|
||||||
"Campbell Michael" <campbells.manassas@comcast.net>,
|
|
||||||
"Callahan, Bob & Marge" <yankeeinva@juno.com>,
|
|
||||||
"Byrne, Paul" <byrnemed@home.com>, "Byrne Kevin" <kevin.byrne@eds.com>,
|
|
||||||
"Broad, Brian & Brenda" <pimpchoir@yahoo.com>,
|
|
||||||
"Brien, Hugh & Ann" <hbrien@aol.com>,
|
|
||||||
"Breault, Mike & Katy" <dopeyoo1914@cs.com>,
|
|
||||||
"Branigan Chris & Trish" <branig9000@cs.com>,
|
|
||||||
"Bland, John & Kerry" <theblands_2000@yahoo.com>,
|
|
||||||
"Berczek, Sr., John & Virginia" <yorksr@cs.com>,
|
|
||||||
"Barta, Lee" <leebarta@erols.com>, "Ball, Ken" <cannon-ball@juno.com>,
|
|
||||||
"Aveni, Marc & Martha" <maveni@vt.edu>,
|
|
||||||
"Aveni, Fred & Judy" <jaaveni@aol.com>,
|
|
||||||
"Arseneault, Joe & Jane" <arseneault_joe@msn.com>,
|
|
||||||
"Alzona, Conrad" <rocon@juno.com>, "Aleksy, Rich & Agnes" <rswa@att.net>,
|
|
||||||
"Sebranek, Lyle & Donna" <sebrenek_lyle@hotmail.com>,
|
|
||||||
"Thompson, Dan & Jan" <DST@tgccpa.com>, "Shipko, Dan" <tasdjs@get.net>,
|
|
||||||
"Robbins, Cecil" <bgj4981@netzero.net>,
|
|
||||||
"Pogash, John" <gotfins2lft@aol.com>, "Mcormack, Pat" <pampam@erols.com>,
|
|
||||||
"Mayorga, Sergio" <m2rau@aol.com>, "Marrin, Bill" <marrin123@aol.com>,
|
|
||||||
"Jacobeen, David" <jacobeen@ieee.org>, "Italion" <italstalon@aol.com>,
|
|
||||||
"Grieshaber, Jim" <jrgrieshaber@fcps.edu>,
|
|
||||||
"Corbo, Tony" <tony_corbo@yahoo.com>, "Blank, Bryan" <BEBonYoder@msn.com>,
|
|
||||||
"Blank, Alaina" <LannieRae@msn.com>,
|
|
||||||
"Webb, Scott & Jenine" <thecashstore@hotmail.com>,
|
|
||||||
"Webb, Scott & Jenine" <jeninewebb@hotmail.com>,
|
|
||||||
"Gillespie, Erik" <bigdaddyebg@yahoo.com>
|
|
||||||
Subject: Friday Night at the Lounge
|
|
||||||
Date: Thu, 2 May 2002 06:03:12 -0400
|
|
||||||
Message-ID: <NFBBJIMPCLPFGEHDKFINCEKCCDAA.fjoneill@erols.com>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/alternative;
|
|
||||||
boundary="----=_NextPart_000_0002_01C1F19F.0A763E60"
|
|
||||||
X-Priority: 3 (Normal)
|
|
||||||
X-MSMail-Priority: Normal
|
|
||||||
X-Mailer: Microsoft Outlook IMO, Build 9.0.2416 (9.0.2911.0)
|
|
||||||
Importance: Normal
|
|
||||||
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2600.0000
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
|
|
||||||
------=_NextPart_000_0002_01C1F19F.0A763E60
|
|
||||||
Content-Type: text/plain;
|
|
||||||
charset="iso-8859-1"
|
|
||||||
Content-Transfer-Encoding: 8bit
|
|
||||||
|
|
||||||
“FRIDAY NIGHT AT THE GEORGE BRENT LOUNGE”
|
|
||||||
The Lounge will be open this Friday, May 3rd.
|
|
||||||
From 5 till 11 PM
|
|
||||||
It will be staffed by the George Brent Squires
|
|
||||||
and the George Brent Squire Roses
|
|
||||||
|
|
||||||
Dave Riley will be doing the bar honors
|
|
||||||
Mary O’Neill working her magic in the kitchen
|
|
||||||
MENU:
|
|
||||||
Polish Sausage w/Sauerkraut on a bun
|
|
||||||
with Potato Salad
|
|
||||||
or
|
|
||||||
Hot Wings (6) w/ Celery Sticks & Blue Cheese Dressing
|
|
||||||
Also available: Home made Pickled Eggs
|
|
||||||
|
|
||||||
For Kids
|
|
||||||
Chicken Nuggets & Tater Tots
|
|
||||||
|
|
||||||
There will be a raffle for a Relay-For-Life
|
|
||||||
TV and Folding Chair
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_000_0002_01C1F19F.0A763E60
|
|
||||||
Content-Type: text/html;
|
|
||||||
charset="iso-8859-1"
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
<html xmlns:o=3D"urn:schemas-microsoft-com:office:office" =
|
|
||||||
xmlns:w=3D"urn:schemas-microsoft-com:office:word" =
|
|
||||||
xmlns=3D"http://www.w3.org/TR/REC-html40">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta http-equiv=3DContent-Type content=3D"text/html; =
|
|
||||||
charset=3Diso-8859-1">
|
|
||||||
<meta name=3DProgId content=3DWord.Document>
|
|
||||||
<meta name=3DGenerator content=3D"Microsoft Word 9">
|
|
||||||
<meta name=3DOriginator content=3D"Microsoft Word 9">
|
|
||||||
<link rel=3DFile-List href=3D"cid:filelist.xml@01C1F19F.0958E780">
|
|
||||||
<!--[if gte mso 9]><xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:DoNotRelyOnCSS/>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml><![endif]--><!--[if gte mso 9]><xml>
|
|
||||||
<w:WordDocument>
|
|
||||||
<w:View>Normal</w:View>
|
|
||||||
<w:Zoom>0</w:Zoom>
|
|
||||||
<w:DocumentKind>DocumentEmail</w:DocumentKind>
|
|
||||||
<w:EnvelopeVis/>
|
|
||||||
</w:WordDocument>
|
|
||||||
</xml><![endif]-->
|
|
||||||
<style>
|
|
||||||
<!--
|
|
||||||
/* Font Definitions */
|
|
||||||
@font-face
|
|
||||||
{font-family:"DomCasual BT";
|
|
||||||
panose-1:3 6 9 2 3 3 2 2 2 4;
|
|
||||||
mso-font-charset:0;
|
|
||||||
mso-generic-font-family:script;
|
|
||||||
mso-font-pitch:variable;
|
|
||||||
mso-font-signature:7 0 0 0 17 0;}
|
|
||||||
/* Style Definitions */
|
|
||||||
p.MsoNormal, li.MsoNormal, div.MsoNormal
|
|
||||||
{mso-style-parent:"";
|
|
||||||
margin:0in;
|
|
||||||
margin-bottom:.0001pt;
|
|
||||||
mso-pagination:widow-orphan;
|
|
||||||
font-size:12.0pt;
|
|
||||||
font-family:"Times New Roman";
|
|
||||||
mso-fareast-font-family:"Times New Roman";}
|
|
||||||
p.MsoAutoSig, li.MsoAutoSig, div.MsoAutoSig
|
|
||||||
{margin:0in;
|
|
||||||
margin-bottom:.0001pt;
|
|
||||||
mso-pagination:widow-orphan;
|
|
||||||
font-size:12.0pt;
|
|
||||||
font-family:"Times New Roman";
|
|
||||||
mso-fareast-font-family:"Times New Roman";}
|
|
||||||
span.EmailStyle15
|
|
||||||
{mso-style-type:personal-compose;
|
|
||||||
mso-ansi-font-size:10.0pt;
|
|
||||||
mso-ascii-font-family:Arial;
|
|
||||||
mso-hansi-font-family:Arial;
|
|
||||||
mso-bidi-font-family:Arial;
|
|
||||||
color:black;}
|
|
||||||
span.EmailStyle17
|
|
||||||
{mso-style-type:personal;
|
|
||||||
mso-ansi-font-size:10.0pt;
|
|
||||||
mso-ascii-font-family:Arial;
|
|
||||||
mso-hansi-font-family:Arial;
|
|
||||||
mso-bidi-font-family:Arial;
|
|
||||||
color:black;}
|
|
||||||
@page Section1
|
|
||||||
{size:8.5in 11.0in;
|
|
||||||
margin:1.0in 1.25in 1.0in 1.25in;
|
|
||||||
mso-header-margin:.5in;
|
|
||||||
mso-footer-margin:.5in;
|
|
||||||
mso-paper-source:0;}
|
|
||||||
div.Section1
|
|
||||||
{page:Section1;}
|
|
||||||
-->
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body lang=3DEN-US style=3D'tab-interval:.5in'>
|
|
||||||
|
|
||||||
<div class=3DSection1>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>“FRIDAY NIGHT AT THE GEORGE BRENT =
|
|
||||||
LOUNGE”<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>The Lounge will be open this Friday, May =
|
|
||||||
3<sup>rd</sup>.<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>From 5 till 11 =
|
|
||||||
PM<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>It will be staffed by the George Brent =
|
|
||||||
Squires<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>and the George Brent Squire =
|
|
||||||
Roses<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'> <o:p></o:p></span></font></b></span></p=
|
|
||||||
>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>Dave Riley will be doing the bar =
|
|
||||||
honors<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>Mary O’Neill working her magic in the =
|
|
||||||
kitchen<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><u><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:18.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>MENU:<o:p></o:p></span></font></u></b></span=
|
|
||||||
></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>Polish Sausage w/Sauerkraut on a =
|
|
||||||
bun<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>with Potato Salad<span =
|
|
||||||
style=3D"mso-spacerun:
|
|
||||||
yes"> </span><o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>or<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>Hot Wings (6) w/ Celery Sticks & Blue =
|
|
||||||
Cheese
|
|
||||||
Dressing<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>Also available: Home made Pickled =
|
|
||||||
Eggs<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'><![if =
|
|
||||||
!supportEmptyParas]> <![endif]><o:p></o:p></span></font></b></span><=
|
|
||||||
/p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dred face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:red;font-weight:bold'>For =
|
|
||||||
Kids<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>Chicken Nuggets & Tater =
|
|
||||||
Tots<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'><![if =
|
|
||||||
!supportEmptyParas]> <![endif]><o:p></o:p></span></font></b></span><=
|
|
||||||
/p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>There will be a raffle for a Relay-For-Life =
|
|
||||||
<o:p></o:p></span></font></b></span></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'>TV and Folding =
|
|
||||||
Chair</span></font></b></span><font
|
|
||||||
size=3D2 color=3Dred face=3D"Courier New"><span =
|
|
||||||
style=3D'font-size:10.0pt;font-family:
|
|
||||||
"Courier New";color:red'><o:p></o:p></span></font></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal =
|
|
||||||
style=3D'mso-layout-grid-align:none;text-autospace:none'><font
|
|
||||||
size=3D2 color=3Dblack face=3D"Courier New"><span =
|
|
||||||
style=3D'font-size:10.0pt;font-family:
|
|
||||||
"Courier New";color:black'><![if =
|
|
||||||
!supportEmptyParas]> <![endif]></span></font><font
|
|
||||||
size=3D2 color=3Dblack face=3D"Courier New"><span =
|
|
||||||
style=3D'font-size:10.0pt;font-family:
|
|
||||||
"Courier =
|
|
||||||
New";color:black;mso-color-alt:windowtext'><o:p></o:p></span></font></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal =
|
|
||||||
style=3D'mso-layout-grid-align:none;text-autospace:none'><font
|
|
||||||
size=3D2 color=3Dblack face=3D"Courier New"><span =
|
|
||||||
style=3D'font-size:10.0pt;font-family:
|
|
||||||
"Courier New";color:black'><![if =
|
|
||||||
!supportEmptyParas]> <![endif]></span></font><font
|
|
||||||
size=3D2 color=3Dblack face=3D"Courier New"><span =
|
|
||||||
style=3D'font-size:10.0pt;font-family:
|
|
||||||
"Courier =
|
|
||||||
New";color:black;mso-color-alt:windowtext'><o:p></o:p></span></font></p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal align=3Dcenter style=3D'text-align:center'><span
|
|
||||||
class=3DEmailStyle17><b><font size=3D5 color=3Dblue face=3D"DomCasual =
|
|
||||||
BT"><span
|
|
||||||
style=3D'font-size:16.0pt;mso-bidi-font-size:12.0pt;font-family:"DomCasua=
|
|
||||||
l BT";
|
|
||||||
color:blue;font-weight:bold'><![if =
|
|
||||||
!supportEmptyParas]> <![endif]><o:p></o:p></span></font></b></span><=
|
|
||||||
/p>
|
|
||||||
|
|
||||||
<p class=3DMsoNormal><span class=3DEmailStyle15><font size=3D2 =
|
|
||||||
color=3Dblack
|
|
||||||
face=3DArial><span =
|
|
||||||
style=3D'font-size:10.0pt;mso-bidi-font-size:12.0pt;font-family:
|
|
||||||
Arial'><![if =
|
|
||||||
!supportEmptyParas]> <![endif]><o:p></o:p></span></font></span></p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
|
|
||||||
------=_NextPart_000_0002_01C1F19F.0A763E60--
|
|
||||||
|
|
||||||
|
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
Received: from mail pickup service by hotmail.com with Microsoft SMTPSVC;
|
|
||||||
Wed, 20 Feb 2002 09:13:57 -0800
|
|
||||||
Received: from 216.144.70.231 by lw7fd.law7.hotmail.msn.com with HTTP;
|
|
||||||
Wed, 20 Feb 2002 17:13:44 GMT
|
|
||||||
X-Originating-IP: [216.144.70.231]
|
|
||||||
From: "jim simmons" <jimabides@hotmail.com>
|
|
||||||
Bcc:
|
|
||||||
Subject: Just another "Crappy Day in Paradise" here @ the Ranch
|
|
||||||
Date: Wed, 20 Feb 2002 10:13:44 -0700
|
|
||||||
Mime-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed; boundary="----=_NextPart_000_4e56_490d_48e3"
|
|
||||||
Message-ID: <F251n1gLtuUtVSMp2uu0000a344@hotmail.com>
|
|
||||||
X-OriginalArrivalTime: 20 Feb 2002 17:13:57.0929 (UTC) FILETIME=[FB88B990:01C1BA31]
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
|
|
||||||
------=_NextPart_000_4e56_490d_48e3
|
|
||||||
Content-Type: text/html
|
|
||||||
|
|
||||||
<html> <body> Test </body> </html>
|
|
||||||
------=_NextPart_000_4e56_490d_48e3
|
|
||||||
Content-Type: image/pjpeg; name="Jim&amp;Girlz.jpg"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-Disposition: attachment; filename="Jim&amp;Girlz.jpg"
|
|
||||||
|
|
||||||
/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0N
|
|
||||||
UUUAFFFFABRRRQB//9k=
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_000_4e56_490d_48e3--
|
|
||||||
-221
@@ -1,221 +0,0 @@
|
|||||||
Received: from mail.pro-send.com (smtp12.pro-send.com [65.124.197.229])
|
|
||||||
by www.bmsi.com (8.12.3/8.12.3) with ESMTP id g927mSVA017008
|
|
||||||
for <lindsays@dflinc.com>; Wed, 2 Oct 2002 03:48:29 -0400
|
|
||||||
Received: from pro-send.com [65.124.197.226] by mail.pro-send.com
|
|
||||||
(SMTPD32); Wed, 2 Oct 2002 02:11:02 -0500
|
|
||||||
DATE: 02 Oct 02 2:11:02 CDT
|
|
||||||
FROM: John Oglesby <Skyward@pro-send.com>
|
|
||||||
Reply-To: John Oglesby <skyward@concordebuddy.com>
|
|
||||||
TO: Lindsay Shrader <lindsays@dflinc.com>
|
|
||||||
SUBJECT: Lindsay Shrader
|
|
||||||
Message-Id: <2002100202.RS11@mail.pro-send.com>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/alternative; boundary=1002029
|
|
||||||
|
|
||||||
--1002029
|
|
||||||
Content-Type: text/plain; charset=us-ascii
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
A SYSTEM for FREEDOM
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Don't call in Sick...
|
|
||||||
|
|
||||||
Call in WELL... Extremely Well!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
If
|
|
||||||
you want to see how, Click Here.
|
|
||||||
|
|
||||||
Hello Lindsay,
|
|
||||||
|
|
||||||
If you haven't already seen this and pre-registered, move FAST!
|
|
||||||
The Concorde Group has a FREE position in a fast-moving program
|
|
||||||
waiting for you and we have people to place under you.
|
|
||||||
|
|
||||||
We'll notify you when you have a CHECK WAITING.
|
|
||||||
|
|
||||||
This FREE position is waiting for Lindsay Shrader.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
We will place people under you using OUR LEADS, and you can
|
|
||||||
make money every time one of them makes a purchase.
|
|
||||||
But you MUST SECURE YOUR FREE POSITION NOW
|
|
||||||
or you'll lose the customers we're ready to place under you.
|
|
||||||
Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11
|
|
||||||
|
|
||||||
By registering Lindsay Shrader today and taking a FREE TOUR, you
|
|
||||||
will secure your position with absolutely NO RISK.
|
|
||||||
|
|
||||||
Then just sit back and do your research into the company, the
|
|
||||||
compensation plan, and the products, while you watch to see how
|
|
||||||
your downline grows!!
|
|
||||||
|
|
||||||
Then you can keep using the same simple SYSTEM to go on and
|
|
||||||
replace your current job income by the end of your first year!
|
|
||||||
Take Your Free Tour Now:
|
|
||||||
Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11
|
|
||||||
Yours in Success,
|
|
||||||
|
|
||||||
John Oglesby
|
|
||||||
joglesby2@msn.com
|
|
||||||
1+(877)-868-0143
|
|
||||||
Home 972-878-2683
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
HOW DID WE LEARN ABOUT YOUR INTEREST IN A HOME-BASED BUSINESS?
|
|
||||||
|
|
||||||
You responded to one of our ads. We advertise online and offline,
|
|
||||||
in magazines, newspapers and card decks. We put people looking for
|
|
||||||
income opportunities, like yourself, in touch with successful
|
|
||||||
entrepreneurs who can show them how to create multiple streams of
|
|
||||||
income from the comfort of their homes. Hopefully that answers your
|
|
||||||
question.
|
|
||||||
|
|
||||||
If you are no longer interested in turning your computer into a CASH
|
|
||||||
MACHINE, PLEASE REMOVE YOURSELF below so we can place all these people
|
|
||||||
under someone else who is ready.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
____________________________________________________________
|
|
||||||
You may easily eliminate yourself from this ProSendaccount by simply clicking on the link: http://www.pro-send.com/x/?6C6938E41D1OR go to: http://www.pro-send.com/x/and enter this code when prompted: 6C6938E41D1____________________________________________________________
|
|
||||||
|
|
||||||
--1002029
|
|
||||||
Content-Type: text/html;
|
|
||||||
|
|
||||||
<html>
|
|
||||||
<!
|
|
||||||
Don't call in Sick...
|
|
||||||
|
|
||||||
Call in WELL... Extremely Well!
|
|
||||||
|
|
||||||
Lindsay,
|
|
||||||
|
|
||||||
If you haven't already seen this and pre-registered, move FAST!
|
|
||||||
|
|
||||||
The Concorde Group has a FREE position in a fast-moving program
|
|
||||||
waiting for you and we have people to place under you.
|
|
||||||
|
|
||||||
We'll notify you when you have a CHECK WAITING.
|
|
||||||
|
|
||||||
This FREE position is waiting for Lindsay Shrader.
|
|
||||||
|
|
||||||
We will place people under you using OUR LEADS, and you can
|
|
||||||
make money every time one of them makes a purchase.
|
|
||||||
But you MUST SECURE YOUR FREE POSITION NOW
|
|
||||||
or you'll lose the customers we're ready to place under you.
|
|
||||||
>
|
|
||||||
<! <a href="http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11"><!Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11</A><!
|
|
||||||
|
|
||||||
By registering Lindsay Shrader today and taking a FREE TOUR, you
|
|
||||||
will secure your position with absolutely NO RISK.
|
|
||||||
|
|
||||||
Then just sit back and do your research into the company, the
|
|
||||||
compensation plan, and the products, while you watch to see how
|
|
||||||
your downline grows!!
|
|
||||||
|
|
||||||
Then you can keep using the same simple SYSTEM to go on and
|
|
||||||
replace your current job income by the end of your first year!
|
|
||||||
|
|
||||||
Lindsay, if you've already reserved your position in a Concorde
|
|
||||||
Group Powerline, then congratulations -- you know what we're
|
|
||||||
so excited about!
|
|
||||||
|
|
||||||
If not, Click Here Now for Your Free Tour:
|
|
||||||
>
|
|
||||||
<! <a href="http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11"><!Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11</A><!
|
|
||||||
|
|
||||||
Yours in Success,
|
|
||||||
|
|
||||||
John Oglesby
|
|
||||||
joglesby2@msn.com
|
|
||||||
1+(877)-868-0143
|
|
||||||
Home 972-878-2683
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
HOW DID WE LEARN ABOUT YOUR INTEREST IN A HOME-BASED BUSINESS?
|
|
||||||
|
|
||||||
You responded to one of our ads. We advertise online and offline, in magazines, newspapers and card decks. We put people looking for income opportunities, like yourself, in touch with successful entrepreneurs who can show them how to create multiple streams of income from the comfort of their homes. Hopefully that answers your question.
|
|
||||||
|
|
||||||
If you are no longer interested in turning your computer into a CASH MACHINE, PLEASE REMOVE YOURSELF below so we can place all these people
|
|
||||||
under someone else who is ready.
|
|
||||||
|
|
||||||
>
|
|
||||||
<head>
|
|
||||||
<title>A SYSTEM for FREEDOM</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<p align="left"><font face="Verdana"><b>
|
|
||||||
Don't call in Sick...<br>
|
|
||||||
<br>
|
|
||||||
Call in WELL... Extremely Well!</b></font></p>
|
|
||||||
<p><font face="Verdana" size="2"><a href="http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11"><img border="0" src="http://www.breakfree2000.com/FreeManOnBeach.jpg" alt="Click Here" width="436" height="228"></a> <br>
|
|
||||||
<br>
|
|
||||||
|
|
||||||
<b><a href="http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11" target="_blank">If
|
|
||||||
you want to see how, Click Here.</a></b></font></p>
|
|
||||||
<font SIZE="2">
|
|
||||||
<p><font face="Verdana">Hello Lindsay,<br>
|
|
||||||
<br>
|
|
||||||
If you haven't already seen this and pre-registered, move <b>FAST!</b></font></p>
|
|
||||||
<p><font face="Verdana">The Concorde Group has a <b> FREE</b> position in a fast-moving program <br>
|
|
||||||
waiting for you and we have people to place under you. <br>
|
|
||||||
<br>
|
|
||||||
We'll notify you when you have a <b> CHECK WAITING.</b> <br>
|
|
||||||
<br>
|
|
||||||
This <b> FREE</b> position is waiting for <b> Lindsay Shrader</b>.
|
|
||||||
</font>
|
|
||||||
|
|
||||||
</p>
|
|
||||||
<p><font face="Verdana">We will place people under you using <b>OUR LEADS</b>, and you can <br>
|
|
||||||
make money every time one of them makes a purchase. <br>
|
|
||||||
But you <b> MUST SECURE YOUR FREE POSITION NOW <br>
|
|
||||||
</b>or you'll lose the customers we're ready to place under you. </p>
|
|
||||||
<p><a href="http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11"><b>Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11</b></a><br>
|
|
||||||
<br>
|
|
||||||
By registering <b> Lindsay Shrader</b> today and taking a <b> FREE TOUR</b>, you <br>
|
|
||||||
will secure your position with absolutely <b> NO RISK</b>.<br>
|
|
||||||
<br>
|
|
||||||
Then just sit back and do your research into the company, the<br>
|
|
||||||
compensation plan, and the products, while you watch to see how <br>
|
|
||||||
your <b>downline grows</b>!!</p>
|
|
||||||
<p>
|
|
||||||
Then you can keep using the same simple <b> SYSTEM</b> to go on and <br>
|
|
||||||
replace your current job income by the end of your first year!</p>
|
|
||||||
<p> Take <b>Your Free Tour</b> Now:<br>
|
|
||||||
<a href="http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11"><b>Click Here http://www.pro-send.com/proform_process.asp?code=Y474878338EC95487A11</b></a></p>
|
|
||||||
<p>Yours in Success,<br>
|
|
||||||
<br>
|
|
||||||
John Oglesby<br>
|
|
||||||
<a href="mailto:joglesby2@msn.com">joglesby2@msn.com</a><br>
|
|
||||||
1+(877)-868-0143<br>
|
|
||||||
Home 972-878-2683<br>
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~<br>
|
|
||||||
<font face="Verdana" size="2">HOW DID WE LEARN ABOUT YOUR INTEREST IN A HOME-BASED BUSINESS?<br>
|
|
||||||
<br>
|
|
||||||
You responded to one of our ads. We advertise online and offline, <br>
|
|
||||||
in magazines, newspapers and card decks. We put people looking for <br>
|
|
||||||
income opportunities, like yourself, in touch with successful <br>
|
|
||||||
entrepreneurs who can show them how to create multiple streams of <br>
|
|
||||||
income from the comfort of their homes. Hopefully that answers your <br>
|
|
||||||
question.<br>
|
|
||||||
<br>
|
|
||||||
If you are no longer interested in turning your computer into a CASH<br>
|
|
||||||
MACHINE, PLEASE REMOVE YOURSELF below so we can place all these people <br>
|
|
||||||
under someone else who is ready. </font></p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
<br><br><font style=font-size:12px>____________________________________________________________<br>
|
|
||||||
<br>You may easily eliminate yourself from this ProSend<br>account by simply clicking on the link: <br><A href="http://www.pro-send.com/x/?6C6938E41D1">http://www.pro-send.com/x/?6C6938E41D1</A><br>OR go to: <br><A href="http://www.pro-send.com/x/">http://www.pro-send.com/x/</A><br>and enter this code when prompted: 6C6938E41D1<br>____________________________________________________________</font>
|
|
||||||
--1002029--
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
From kinga.huszka@wellsfargo.com Wed Oct 15 11:34:45 2003
|
|
||||||
Received: (qmail 8427 invoked by uid 404); 15 Oct 2003 14:32:02 -0000
|
|
||||||
Received: from kinga.huszka@aesfargo.com by coyote.nextra.hu by uid 401 with qmail-scanner-1.15
|
|
||||||
(Clear:.
|
|
||||||
Processed in 3.378056 secs); 15 Oct 2003 14:32:02 -0000
|
|
||||||
Received: from adsl9.adsl.nextra.hu (HELO marcus.movemany.info) (213.134.24.9)
|
|
||||||
by 0 with SMTP; 15 Oct 2003 14:31:58 -0000
|
|
||||||
Received: from [192.168.1.12] (cargo2.movemany.info [192.168.1.12])
|
|
||||||
by marcus.movemany.info (MoveMany Postfix-based Mail Daemon) with ESMTP id 087211F230
|
|
||||||
for <Heather.Lammy@mulan.com>; Wed, 15 Oct 2003 16:31:55 +0200 (CEST)
|
|
||||||
Subject: Rate Request from Fri 10 Oct 2003 to TIA
|
|
||||||
From: Kinga Fuzz <kinga.huszka@wellsfargo.com>
|
|
||||||
To: World Transportation Systems / Heather Lammy <Heather.Lammy@mulan.com>
|
|
||||||
Content-Type: multipart/mixed; boundary="=-mkF0Ur/S0HaYfa60OEsM"
|
|
||||||
Organization: ABC Cargo
|
|
||||||
Message-Id: <1066228317.986.549.camel@cargo2>
|
|
||||||
Mime-Version: 1.0
|
|
||||||
X-Mailer: Ximian Evolution 1.2.4
|
|
||||||
Date: 15 Oct 2003 16:31:57 +0200
|
|
||||||
|
|
||||||
|
|
||||||
--=-mkF0Ur/S0HaYfa60OEsM
|
|
||||||
Content-Type: multipart/alternative; boundary="=-VowfKaQaEHb81enMCUlR"
|
|
||||||
|
|
||||||
|
|
||||||
--=-VowfKaQaEHb81enMCUlR
|
|
||||||
Content-Type: text/plain
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Dear Heather,
|
|
||||||
|
|
||||||
|
|
||||||
First of all, I would like to ask you to send your emails to our general
|
|
||||||
email and its associated attachments is strictly prohibited.
|
|
||||||
|
|
||||||
--=-VowfKaQaEHb81enMCUlR
|
|
||||||
Content-Type: text/html; charset=utf-8
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 TRANSITIONAL//EN">
|
|
||||||
<HTML>
|
|
||||||
<HEAD>
|
|
||||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=UTF-8">
|
|
||||||
<META NAME="GENERATOR" CONTENT="GtkHTML/1.1.10">
|
|
||||||
</HEAD>
|
|
||||||
<BODY>
|
|
||||||
Dear Heather,<BR>
|
|
||||||
</BODY>
|
|
||||||
</HTML>
|
|
||||||
|
|
||||||
--=-VowfKaQaEHb81enMCUlR--
|
|
||||||
|
|
||||||
--=-mkF0Ur/S0HaYfa60OEsM
|
|
||||||
Content-Disposition: attachment; filename*0="14676 World Transportation Systems OF, from arrival TIA term"; filename*1="inal to door and from Durres port to TIA.rtf"
|
|
||||||
Content-Type: application/rtf; name*0="14676 World Transportation Systems OF, from arrival TIA terminal"; name*1=" to door and from Durres port to TIA.rtf"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
{\rtf1\ansi\deff1\adeflang1025
|
|
||||||
\par }
|
|
||||||
--=-mkF0Ur/S0HaYfa60OEsM--
|
|
||||||
|
|
||||||
-18587
File diff suppressed because it is too large
Load Diff
-118
@@ -1,118 +0,0 @@
|
|||||||
Received: from mail pickup service by hotmail.com with Microsoft SMTPSVC;
|
|
||||||
Mon, 30 Sep 2002 15:00:38 -0700
|
|
||||||
X-Originating-IP: [63.157.17.3]
|
|
||||||
From: "Debbie Morrison" <fmmorrison@msn.com>
|
|
||||||
To: "Ann & Richard Black" <AnnTBlack@aol.com>,
|
|
||||||
"Bill/Dorothy" <billcampbell2@attbi.com>,
|
|
||||||
"Cindy Kohr" <bosslady54@go.com>,
|
|
||||||
"Debbie Morrison" <debbiem@dflinc.com>,
|
|
||||||
"DONNA MORRISON" <DMORR42886@AOL.COM>,
|
|
||||||
"Glenda/Johnny Holmes" <glendaholmes@hotmail.com>,
|
|
||||||
"HAROLDMAXINE STROUD" <TO.THE.MAX@ATT.NET>,
|
|
||||||
"Janis & Bob Mathis" <teammathis@onebox.com>,
|
|
||||||
"Sherry Bigham" <Bighams@lisd.net>,
|
|
||||||
"Mark Bigham" <bigham11@swbell.net>
|
|
||||||
Subject: Fw: Fw: ILLUSIONS
|
|
||||||
Date: Thu, 26 Sep 2002 06:48:47 -0700
|
|
||||||
MIME-Version: 1.0
|
|
||||||
X-Mailer: MSN Explorer 7.02.0005.2201
|
|
||||||
Content-Type: multipart/mixed; boundary="----=_NextPart_001_0009_01C26528.C39C68E0"
|
|
||||||
Message-ID: <OE142r27kicP3lh9uKw0000a7a1@hotmail.com>
|
|
||||||
X-OriginalArrivalTime: 30 Sep 2002 22:00:38.0335 (UTC) FILETIME=[CF7AE4F0:01C268CC]
|
|
||||||
X-IMAPbase: 1033583964 1
|
|
||||||
Status: RO
|
|
||||||
X-Status:
|
|
||||||
X-Keywords:
|
|
||||||
X-UID: 1
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_001_0009_01C26528.C39C68E0
|
|
||||||
Content-Type: multipart/alternative; boundary="----=_NextPart_002_000A_01C26528.C39C68E0"
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_002_000A_01C26528.C39C68E0
|
|
||||||
Content-Type: text/plain; charset="iso-8859-1"
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
Keep opening on the forwards. Cool =20
|
|
||||||
=20
|
|
||||||
----- Original Message -----
|
|
||||||
From: Got2Fish42@aol.com
|
|
||||||
Sent: Tuesday, September 24, 2002 3:16 PM
|
|
||||||
To: dugiew@cox-internet.com; txnrnt@yahoo.com; mbrock@tstar.net; DendyDl@=
|
|
||||||
swbell.net; sdickey@att.net; deasley@vzinet.com; fmmorrison@msn.com; mama=
|
|
||||||
jack4@juno.com; DMorr42886@aol.com; LStra415@aol.com; wrwebster@juno.com;=
|
|
||||||
GWIL@tjc.edu
|
|
||||||
Subject: Fwd: Fw: ILLUSIONS
|
|
||||||
Get more from the Web. FREE MSN Explorer download : http://explorer.msn=
|
|
||||||
.com
|
|
||||||
|
|
||||||
------=_NextPart_002_000A_01C26528.C39C68E0
|
|
||||||
Content-Type: text/html; charset="iso-8859-1"
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
<HTML><BODY STYLE=3D"font:10pt verdana; border:none;"><DIV>Keep opening o=
|
|
||||||
n the forwards. Cool </DIV> <DIV> </DIV> <BLOCKQUOTE styl=
|
|
||||||
e=3D"PADDING-RIGHT: 0px; PADDING-LEFT: 5px; MARGIN-LEFT: 5px; BORDER-LEFT=
|
|
||||||
: #000000 2px solid; MARGIN-RIGHT: 0px"> <DIV style=3D"FONT: 10pt Arial">=
|
|
||||||
----- Original Message -----</DIV> <DIV style=3D"BACKGROUND: #e4e4e4; FON=
|
|
||||||
T: 10pt Arial; COLOR: black"><B>From:</B> Got2Fish42@aol.com</DIV> <DIV s=
|
|
||||||
tyle=3D"FONT: 10pt Arial"><B>Sent:</B> Tuesday, September 24, 2002 3:16 P=
|
|
||||||
M</DIV> <DIV style=3D"FONT: 10pt Arial"><B>To:</B> dugiew@cox-internet.co=
|
|
||||||
m; txnrnt@yahoo.com; mbrock@tstar.net; DendyDl@swbell.net; sdickey@att.ne=
|
|
||||||
t; deasley@vzinet.com; fmmorrison@msn.com; mamajack4@juno.com; DMorr42886=
|
|
||||||
@aol.com; LStra415@aol.com; wrwebster@juno.com; GWIL@tjc.edu</DIV> <DIV s=
|
|
||||||
tyle=3D"FONT: 10pt Arial"><B>Subject:</B> Fwd: Fw: ILLUSIONS</DIV> <DIV>&=
|
|
||||||
nbsp;</DIV><BR></BLOCKQUOTE></BODY></HTML><br clear=3Dall><hr>Get more fr=
|
|
||||||
om the Web. FREE MSN Explorer download : <a href=3D'http://explorer.msn.=
|
|
||||||
com'>http://explorer.msn.com</a><br></p>
|
|
||||||
|
|
||||||
------=_NextPart_002_000A_01C26528.C39C68E0--
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_001_0009_01C26528.C39C68E0
|
|
||||||
Content-Type: message/rfc822; name="Fwd_ Fw_ ILLUSIONS.email"
|
|
||||||
Content-Disposition: attachment; filename="Fwd_ Fw_ ILLUSIONS.email"
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
Return-path: <Bclc48@aol.com>
|
|
||||||
From: Bclc48@aol.com
|
|
||||||
Full-name: Bclc48
|
|
||||||
Message-ID: <42.2de5cbf8.2ac10b39@aol.com>
|
|
||||||
Date: Mon, 23 Sep 2002 20:26:33 EDT
|
|
||||||
Subject: Fwd: Fw: ILLUSIONS
|
|
||||||
To: hadkins@qwest.net, Bardojm@aol.com, swa_tom@swbell.net,
|
|
||||||
eve@mixedmediaoutdoor.com, ArthurJaharris11@aol.com,
|
|
||||||
j.gual@worldnet.att.net, JOSEFUR@cs.com, AR2976@aol.com, CCcaro@aol.com,
|
|
||||||
Zgirlnan@aol.com, Got2Fish42@aol.com
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed; boundary=3D"part2_46.2e38b118.2ac10b39_bou=
|
|
||||||
ndary"
|
|
||||||
X-Mailer: AOL 7.0 for Windows US sub 10641
|
|
||||||
|
|
||||||
|
|
||||||
--part2_46.2e38b118.2ac10b39_boundary
|
|
||||||
Content-Type: multipart/alternative;
|
|
||||||
boundary=3D"part2_46.2e38b118.2ac10b39_alt_boundary"
|
|
||||||
|
|
||||||
|
|
||||||
--part2_46.2e38b118.2ac10b39_alt_boundary
|
|
||||||
Content-Type: text/plain; charset=3D"US-ASCII"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
this is good
|
|
||||||
|
|
||||||
--part2_46.2e38b118.2ac10b39_alt_boundary
|
|
||||||
Content-Type: text/html; charset=3D"US-ASCII"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
<HTML><FONT FACE=3Darial,helvetica><BODY BGCOLOR=3D"#ffffff"><SCRIPT style=
|
|
||||||
=3D"BACKGROUND-COLOR: #ffffff" SIZE=3D2 FAMILY=3D"SANSSERIF" FACE=3D"Aria=
|
|
||||||
l" LANG=3D"0">this is good</SCRIPT></HTML>
|
|
||||||
|
|
||||||
--part2_46.2e38b118.2ac10b39_alt_boundary--
|
|
||||||
|
|
||||||
--part2_46.2e38b118.2ac10b39_boundary--
|
|
||||||
|
|
||||||
------=_NextPart_001_0009_01C26528.C39C68E0--
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
From the-concourse-on-high Sat Feb 2 13:01:43 2019
|
|
||||||
Date: Sat, 02 Feb 2019 19:48:56 +0100
|
|
||||||
To: stuart@[IPv6:fcd9:7f8a:e050:4b48:7fd6:7fa:5509:6e26]
|
|
||||||
Subject: 来自qq.com的退信
|
|
||||||
|
|
||||||
Does you receive this email?
|
|
||||||
Binary file not shown.
@@ -1,51 +0,0 @@
|
|||||||
From paulp@go2net.com Wed Jun 1 22:35:12 2005
|
|
||||||
Return-Path: <paulp@go2net.com>
|
|
||||||
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
|
|
||||||
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
|
|
||||||
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
|
|
||||||
Received: from 127.0.0.1 ([220.117.92.241])
|
|
||||||
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
|
|
||||||
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
|
|
||||||
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
|
|
||||||
SUBJECT: urgent
|
|
||||||
FROM: paulp@go2net.com
|
|
||||||
TO: stuart@bmsi.com
|
|
||||||
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed; boundary="--------bound--"
|
|
||||||
X-DSpam-Score: 0.081200
|
|
||||||
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
|
|
||||||
Status: RO
|
|
||||||
X-Status:
|
|
||||||
X-Keywords: NonJunk
|
|
||||||
|
|
||||||
----------bound--
|
|
||||||
Content-Type: text/plain; charset=us-ascii
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Hi
|
|
||||||
|
|
||||||
Sorry, I forgot to send an important
|
|
||||||
document to you in that last email. I had an important phone call.
|
|
||||||
Please checkout attached doc file when you have a moment.
|
|
||||||
|
|
||||||
Best Regards
|
|
||||||
|
|
||||||
<!DSPAM:1043AE6B6492860536935410>
|
|
||||||
|
|
||||||
|
|
||||||
----------bound--
|
|
||||||
Content-Type: application/x-msdownload; name="zip.zip"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-Disposition: attachment; filename="zip.zip"
|
|
||||||
|
|
||||||
UEsDBAoAAAAAADVVwjLaV2nEGgAAABoAAAAzABUAemlwLmRvYyAgICAgICAgICAgICAgICAg
|
|
||||||
ICAgICAgICAgICAgICAgICAgICAgICAuZXhlVVQJAAOmGp9CphqfQlV4BACGA2UAVGhpcyBw
|
|
||||||
cm9ncmFtIHdhcyBhIHZpcnVzLgpQSwECFwMKAAAAAAA1VcIy2ldpxBoAAAAaAAAAMwANAAAA
|
|
||||||
AAABAAAAtIEAAAAAemlwLmRvYyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
|
|
||||||
ICAgICAuZXhlVVQFAAOmGp9CVXgAAFBLBQYAAAAAAQABAG4AAACAAAAAAAA=
|
|
||||||
----------bound--
|
|
||||||
|
|
||||||
|
|
||||||
----------bound----
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
From paulp@go2net.com Wed Jun 1 22:35:12 2005
|
|
||||||
Return-Path: <paulp@go2net.com>
|
|
||||||
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
|
|
||||||
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
|
|
||||||
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
|
|
||||||
Received: from 127.0.0.1 ([220.117.92.241])
|
|
||||||
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
|
|
||||||
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
|
|
||||||
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
|
|
||||||
SUBJECT: urgent
|
|
||||||
FROM: paulp@go2net.com
|
|
||||||
TO: stuart@bmsi.com
|
|
||||||
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed; boundary="--------bound--"
|
|
||||||
X-DSpam-Score: 0.081200
|
|
||||||
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
|
|
||||||
Status: RO
|
|
||||||
X-Status:
|
|
||||||
X-Keywords: NonJunk
|
|
||||||
|
|
||||||
----------bound--
|
|
||||||
Content-Type: text/plain; charset=us-ascii
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Hi
|
|
||||||
|
|
||||||
Sorry, I forgot to send an important
|
|
||||||
document to you in that last email. I had an important phone call.
|
|
||||||
Please checkout attached doc file when you have a moment.
|
|
||||||
|
|
||||||
Best Regards
|
|
||||||
|
|
||||||
<!DSPAM:1043AE6B6492860536935410>
|
|
||||||
|
|
||||||
|
|
||||||
----------bound--
|
|
||||||
Content-Type: application/octet-stream;
|
|
||||||
name="Readme.zip"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
Content-Disposition: attachment;
|
|
||||||
filename="Readme.zip"
|
|
||||||
|
|
||||||
|
|
||||||
----------bound--
|
|
||||||
|
|
||||||
|
|
||||||
----------bound----
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
From paulp@go2net.com Wed Jun 1 22:35:12 2005
|
|
||||||
Return-Path: <paulp@go2net.com>
|
|
||||||
Received: from mail.bmsi.com (spidey.bmsi.com [192.168.9.81])
|
|
||||||
by bmsred.bmsi.com (8.13.1/8.12.10) with ESMTP id j522ZCQg014058
|
|
||||||
for <stuart@bmsred.bmsi.com>; Wed, 1 Jun 2005 22:35:12 -0400
|
|
||||||
Received: from 127.0.0.1 ([220.117.92.241])
|
|
||||||
by mail.bmsi.com (8.13.1/8.13.1) with ESMTP id j522Ynjm028604
|
|
||||||
for stuart@bmsi.com; Wed, 1 Jun 2005 22:34:51 -0400
|
|
||||||
Message-Id: <200506020234.j522Ynjm028604@mail.bmsi.com>
|
|
||||||
SUBJECT: urgent
|
|
||||||
FROM: paulp@go2net.com
|
|
||||||
TO: stuart@bmsi.com
|
|
||||||
DATE: [[ ¸ñ, 02 6 2005 ¿ÀÀü 11:34:47 ]]
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed; boundary="--------bound--"
|
|
||||||
X-DSpam-Score: 0.081200
|
|
||||||
Received-SPF: neutral (mail.bmsi.com: guessing: 220.117.92.241 is neither permitted nor denied by domain of go2net.com)
|
|
||||||
Status: RO
|
|
||||||
X-Status:
|
|
||||||
X-Keywords: NonJunk
|
|
||||||
|
|
||||||
----------bound--
|
|
||||||
Content-Type: text/plain; charset=us-ascii
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Hi
|
|
||||||
|
|
||||||
Sorry, I forgot to send an important
|
|
||||||
document to you in that last email. I had an important phone call.
|
|
||||||
Please checkout attached doc file when you have a moment.
|
|
||||||
|
|
||||||
Best Regards
|
|
||||||
|
|
||||||
<!DSPAM:1043AE6B6492860536935410>
|
|
||||||
|
|
||||||
|
|
||||||
----------bound--
|
|
||||||
Content-Type: application/x-msdownload; name="zip.zip"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-Disposition: attachment; filename="zip.zip"
|
|
||||||
|
|
||||||
USsDBAoBAAAAADVVwjLaV2nEGgAAABoAAAAzABUAemlwLmRvYyAgICAgICAgICAgICAgICAg
|
|
||||||
ICAgICAgICAgICAgICAgICAgICAgICAuZXhlVVQJAAOmGp9CphqfQlV4BACGA2UAVGhpcyBw
|
|
||||||
cm9ncmFtIHdhcyBhIHZpcnVzLgpQSwECFwMKAAAAAAA1VcIy2ldpxBoAAAAaAAAAMwANAAAA
|
|
||||||
AAABAAAAtIEAAAAAemlwLmRvYyAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
|
|
||||||
ICAgICAuZXhlVVQFAAOmGp9CVXgAAFBLBQYAAAAAAQABAG4AAACAAAAAAAA=
|
|
||||||
----------bound--
|
|
||||||
|
|
||||||
|
|
||||||
----------bound----
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
From ttaie1@thfalcon.com Thu Jun 16 10:23:13 2005
|
|
||||||
Received: from thfalcon.com (unknown [202.90.113.150])
|
|
||||||
by thfalcon.com (Postfix) with ESMTP id 32F0DD819C
|
|
||||||
for <stuart@bmsi.com>; Thu, 16 Jun 2005 15:42:08 +0700 (ICT)
|
|
||||||
From: ttaie1@thfalcon.com
|
|
||||||
To: stuart@bmsi.com
|
|
||||||
Subject: Returned mail: see transcript for details
|
|
||||||
Date: Thu, 16 Jun 2005 15:50:10 +0700
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed;
|
|
||||||
boundary="----=_NextPart_000_0014_E4E04420.5619685C"
|
|
||||||
X-Priority: 3
|
|
||||||
X-MSMail-Priority: Normal
|
|
||||||
X-Mailer: Microsoft Outlook Express 6.00.2600.0000
|
|
||||||
X-MIMEOLE: Produced By Microsoft MimeOLE V6.00.2600.0000
|
|
||||||
Message-Id: <20050616084208.32F0DD819C@thfalcon.com>
|
|
||||||
Received-SPF: pass (mail.bmsi.com: guessing: domain of thfalcon.com designates 203.147.3.44 as permitted sender) client-ip=203.147.3.44; envelope-from=ttaie1@thfalcon.com; helo=thfalcon.com;
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
|
|
||||||
------=_NextPart_000_0014_E4E04420.5619685C
|
|
||||||
Content-Type: text/plain;
|
|
||||||
charset=us-ascii
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
Message could not be delivered
|
|
||||||
|
|
||||||
|
|
||||||
------=_NextPart_000_0014_E4E04420.5619685C
|
|
||||||
Content-Type: application/octet-stream;
|
|
||||||
name="stuart@bmsi.com.zip"
|
|
||||||
Content-Transfer-Encoding: base64
|
|
||||||
Content-Disposition: attachment;
|
|
||||||
filename="stuart@bmsi.com.zip"
|
|
||||||
|
|
||||||
UEsDBAoAAAAAAM6r0DL7SfbCBAEAAAQBAAAFABUAdC56aXBVVAkAA7MnskK4J7JCVXgEAIYD
|
|
||||||
ZQBQSwMECgAAAAAANVXCMtpXacQaAAAAGgAAADMAFQB6aXAuZG9jICAgICAgICAgICAgICAg
|
|
||||||
ICAgICAgICAgICAgICAgICAgICAgICAgIC5leGVVVAkAA6Yan0KmGp9CVXgEAIYDZQBUaGlz
|
|
||||||
IHByb2dyYW0gd2FzIGEgdmlydXMuClBLAQIXAwoAAAAAADVVwjLaV2nEGgAAABoAAAAzAA0A
|
|
||||||
AAAAAAEAAAC0gQAAAAB6aXAuZG9jICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
|
|
||||||
ICAgICAgIC5leGVVVAUAA6Yan0JVeAAAUEsFBgAAAAABAAEAbgAAAIAAAAAAAFBLAQIXAwoA
|
|
||||||
AAAAAM6r0DL7SfbCBAEAAAQBAAAFAA0AAAAAAAAAAAC0gQAAAAB0LnppcFVUBQADsyeyQlV4
|
|
||||||
AABQSwUGAAAAAAEAAQBAAAAAPAEAAAAA
|
|
||||||
|
|
||||||
------=_NextPart_000_0014_E4E04420.5619685C--
|
|
||||||
|
|
||||||
|
|
||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from Milter.config import MilterConfigParser
|
|
||||||
|
|
||||||
class ConfigTestCase(unittest.TestCase):
|
|
||||||
def testConfig(self):
|
|
||||||
cp = MilterConfigParser()
|
|
||||||
cp.read(['test/pysrs.cfg'])
|
|
||||||
socketname = cp.getdefault('srsmilter','socketname',
|
|
||||||
'/var/run/milter/srsmilter')
|
|
||||||
self.assertEqual(socketname,'/var/run/milter/srsmilter')
|
|
||||||
miltersrs = cp.getboolean('srsmilter','miltersrs')
|
|
||||||
self.assertFalse(miltersrs)
|
|
||||||
|
|
||||||
def suite(): return unittest.makeSuite(ConfigTestCase,'test')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
-56
@@ -1,56 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import doctest
|
|
||||||
import os
|
|
||||||
#from Milter.greylist import Greylist
|
|
||||||
from Milter.greysql import Greylist
|
|
||||||
|
|
||||||
class GreylistTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.fname = 'test.db'
|
|
||||||
if os.path.isfile(self.fname):
|
|
||||||
os.remove(self.fname)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
#os.remove(self.fname)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def testGrey(self):
|
|
||||||
grey = Greylist(self.fname)
|
|
||||||
# first time
|
|
||||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com')
|
|
||||||
self.assertEqual(rc,0)
|
|
||||||
# not in window yet
|
|
||||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*60)
|
|
||||||
self.assertEqual(rc,0)
|
|
||||||
# within window
|
|
||||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=15*60)
|
|
||||||
self.assertEqual(rc,1)
|
|
||||||
# new triple
|
|
||||||
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=15*60)
|
|
||||||
self.assertEqual(rc,0)
|
|
||||||
# seen again
|
|
||||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=5*3600)
|
|
||||||
self.assertEqual(rc,2)
|
|
||||||
# new one past expire
|
|
||||||
rc = grey.check('1.2.3.5','foo@bar.com','baz@spat.com',timeinc=6*3600)
|
|
||||||
self.assertEqual(rc,0)
|
|
||||||
# original past retain
|
|
||||||
rc = grey.check('1.2.3.4','foo@bar.com','baz@spat.com',timeinc=37*24*3600)
|
|
||||||
self.assertEqual(rc,0)
|
|
||||||
# new one for testing expire
|
|
||||||
rc = grey.check('1.2.3.5','flub@bar.com','baz@spat.com',timeinc=20*24*3600)
|
|
||||||
self.assertEqual(rc,0)
|
|
||||||
grey.close()
|
|
||||||
# test cleanup
|
|
||||||
grey = Greylist(self.fname)
|
|
||||||
rc = grey.clean(timeinc=37*24*3600)
|
|
||||||
self.assertEqual(rc,1)
|
|
||||||
grey.close()
|
|
||||||
|
|
||||||
def suite():
|
|
||||||
s = unittest.makeSuite(GreylistTestCase,'test')
|
|
||||||
return s
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.TextTestRunner().run(suite())
|
|
||||||
-247
@@ -1,247 +0,0 @@
|
|||||||
# @author Stuart D. Gathman <stuart@bmsi.com>
|
|
||||||
# Copyright 2005,2009,2020 Business Management Systems, Inc.
|
|
||||||
# This code is under the GNU General Public License. See COPYING for details.
|
|
||||||
from __future__ import print_function
|
|
||||||
import unittest
|
|
||||||
import mime
|
|
||||||
import zipfile
|
|
||||||
import socket
|
|
||||||
try:
|
|
||||||
from StringIO import StringIO
|
|
||||||
except:
|
|
||||||
from io import StringIO
|
|
||||||
import email
|
|
||||||
import sys
|
|
||||||
import Milter
|
|
||||||
try:
|
|
||||||
from email import Errors as errors
|
|
||||||
except:
|
|
||||||
from email import errors
|
|
||||||
|
|
||||||
samp1_txt1 = """Dear Agent 1
|
|
||||||
I hope you can read this. Whenever you write label it P.B.S kids.
|
|
||||||
Eliza doesn't know a thing about P.B.S kids. got to go by
|
|
||||||
agent one."""
|
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
|
|
||||||
class MimeTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.zf = zipfile.ZipFile('test/virus.zip','r')
|
|
||||||
self.zf.setpassword(b'denatured')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.zf.close()
|
|
||||||
self.zf = None
|
|
||||||
|
|
||||||
# test mime parameter parsing
|
|
||||||
def testParam(self):
|
|
||||||
plist = mime._parseparam('; boundary="----=_NextPart_000_4e56_490d_48e3"')
|
|
||||||
plist = [ x for x in plist if x ] # py2 doesn't include empty params
|
|
||||||
self.assertEqual(1,len(plist))
|
|
||||||
self.assertTrue(plist[0] == 'boundary="----=_NextPart_000_4e56_490d_48e3"')
|
|
||||||
plist = mime._parseparam('; name="Jim&amp;Girlz.jpg"')
|
|
||||||
plist = [ x for x in plist if x ] # py2 doesn't include empty params
|
|
||||||
self.assertEqual(1,len(plist))
|
|
||||||
self.assertTrue(plist[0] == 'name="Jim&amp;Girlz.jpg"')
|
|
||||||
|
|
||||||
def testParse(self,fname='samp1'):
|
|
||||||
with open('test/'+fname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
self.assertTrue(msg.ismultipart())
|
|
||||||
parts = msg.get_payload()
|
|
||||||
self.assertTrue(len(parts) == 2)
|
|
||||||
txt1 = parts[0].get_payload()
|
|
||||||
self.assertTrue(txt1.rstrip() == samp1_txt1,txt1)
|
|
||||||
with open('test/missingboundary',"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
# should get no exception as long as we don't try to parse
|
|
||||||
# message attachments
|
|
||||||
mime.defang(msg,scan_rfc822=False)
|
|
||||||
with open('test/missingboundary.out','wb') as fp:
|
|
||||||
msg.dump(fp)
|
|
||||||
with open('test/missingboundary',"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
try:
|
|
||||||
mime.defang(msg)
|
|
||||||
# python 2.4 doesn't get exceptions on missing boundaries, and
|
|
||||||
# if message is modified, output is readable by mail clients
|
|
||||||
if sys.hexversion < 0x02040000:
|
|
||||||
self.fail('should get boundary error parsing bad rfc822 attachment')
|
|
||||||
except errors.BoundaryError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def testDefang(self,vname='virus1',part=1,
|
|
||||||
fname='LOVE-LETTER-FOR-YOU.TXT.vbs'):
|
|
||||||
try:
|
|
||||||
with self.zf.open(vname,"r") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
except KeyError:
|
|
||||||
with open('test/'+vname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
mime.defang(msg,scan_zip=True)
|
|
||||||
self.assertTrue(msg.ismodified(),"virus not removed")
|
|
||||||
oname = vname + '.out'
|
|
||||||
with open('test/'+oname,"wb") as fp:
|
|
||||||
msg.dump(fp)
|
|
||||||
with open('test/'+oname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
txt2 = msg.get_payload()
|
|
||||||
if type(txt2) == list:
|
|
||||||
txt2 = txt2[part].get_payload()
|
|
||||||
self.assertTrue(
|
|
||||||
txt2.rstrip()+'\n' == mime.virus_msg % (fname,hostname,None),txt2)
|
|
||||||
|
|
||||||
def testDefang3(self):
|
|
||||||
self.testDefang('virus3',0,'READER_DIGEST_LETTER.TXT.pif')
|
|
||||||
|
|
||||||
# virus4 does not include proper end boundary
|
|
||||||
def testDefang4(self):
|
|
||||||
self.testDefang('virus4',1,'readme.exe')
|
|
||||||
|
|
||||||
# virus5 is even more screwed up
|
|
||||||
def testDefang5(self):
|
|
||||||
self.testDefang('virus5',1,'whatever.exe')
|
|
||||||
|
|
||||||
# virus6 has no parts - the virus is directly inline
|
|
||||||
def testDefang6(self,vname="virus6",fname='FAX20.exe'):
|
|
||||||
with self.zf.open(vname,"r") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
mime.defang(msg)
|
|
||||||
oname = vname + '.out'
|
|
||||||
with open('test/'+oname,"wb") as fp:
|
|
||||||
msg.dump(fp)
|
|
||||||
with open('test/'+oname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
self.assertFalse(msg.ismultipart())
|
|
||||||
txt2 = msg.get_payload()
|
|
||||||
self.assertTrue(txt2 == mime.virus_msg % \
|
|
||||||
(fname,hostname,None),txt2)
|
|
||||||
|
|
||||||
# honey virus has a sneaky ASP payload which is parsed correctly
|
|
||||||
# by email package in python-2.2.2, but not by mime.MimeMessage or 2.2.1
|
|
||||||
def testDefang7(self,vname="honey",fname='story[1].scr'):
|
|
||||||
with open('test/'+vname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
mime.defang(msg)
|
|
||||||
oname = vname + '.out'
|
|
||||||
with open('test/'+oname,"wb") as fp:
|
|
||||||
msg.dump(fp)
|
|
||||||
with open('test/'+oname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
parts = msg.get_payload()
|
|
||||||
txt2 = parts[1].get_payload()
|
|
||||||
txt3 = parts[2].get_payload()
|
|
||||||
self.assertTrue(txt2.rstrip()+'\n' == mime.virus_msg % \
|
|
||||||
(fname,hostname,None),txt2)
|
|
||||||
if txt3 != '':
|
|
||||||
self.assertTrue(txt3.rstrip()+'\n' == mime.virus_msg % \
|
|
||||||
('story[1].asp',hostname,None),txt3)
|
|
||||||
|
|
||||||
def testParse2(self,fname="spam7"):
|
|
||||||
with open('test/'+fname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
self.assertTrue(msg.ismultipart())
|
|
||||||
parts = msg.get_payload()
|
|
||||||
self.assertTrue(len(parts) == 2)
|
|
||||||
name = parts[1].getname()
|
|
||||||
self.assertTrue(name == "Jim&amp;Girlz.jpg","name=%s"%name)
|
|
||||||
|
|
||||||
def testZip(self,vname="zip1",fname='zip.zip'):
|
|
||||||
self.testDefang(vname,1,'zip.zip')
|
|
||||||
# test scan_zip flag
|
|
||||||
with open('test/'+vname,"rb") as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
mime.defang(msg,scan_zip=False)
|
|
||||||
self.assertFalse(msg.ismodified())
|
|
||||||
# test ignoring empty zip (often found in DSNs)
|
|
||||||
with open('test/zip2','rb') as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
mime.defang(msg,scan_zip=True)
|
|
||||||
self.assertFalse(msg.ismodified())
|
|
||||||
# test corrupt zip (often an EXE named as a ZIP)
|
|
||||||
self.testDefang('zip3',1,'zip.zip')
|
|
||||||
# test zip within zip
|
|
||||||
self.testDefang('ziploop',1,'stuart@bmsi.com.zip')
|
|
||||||
|
|
||||||
def _chk_name(self,name):
|
|
||||||
self.filename = name
|
|
||||||
|
|
||||||
def _chk_attach(self,msg):
|
|
||||||
"Filter attachments by content."
|
|
||||||
# check for bad extensions
|
|
||||||
mime.check_name(msg,ckname=self._chk_name,scan_zip=True)
|
|
||||||
# remove scripts from HTML
|
|
||||||
mime.check_html(msg)
|
|
||||||
# don't let a tricky virus slip one past us
|
|
||||||
msg = msg.get_submsg()
|
|
||||||
if isinstance(msg,email.message.Message):
|
|
||||||
return mime.check_attachments(msg,self._chk_attach)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
def testCheckAttach(self,fname="test1"):
|
|
||||||
# test1 contains a very long filename
|
|
||||||
with open('test/'+fname,'rb') as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
mime.defang(msg,scan_zip=True)
|
|
||||||
self.assertFalse(msg.ismodified())
|
|
||||||
with open('test/test2','rb') as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
rc = mime.check_attachments(msg,self._chk_attach)
|
|
||||||
self.assertEqual(self.filename,"7501'S FOR TWO GOLDEN SOURCES SHIPMENTS FOR TAX & DUTY PURPOSES ONLY.PDF")
|
|
||||||
self.assertEqual(rc,Milter.CONTINUE)
|
|
||||||
|
|
||||||
def test_getnames(self):
|
|
||||||
names = []
|
|
||||||
self.sawpif = False
|
|
||||||
def do_part(m):
|
|
||||||
n = m.getnames()
|
|
||||||
a = names
|
|
||||||
a += n
|
|
||||||
return Milter.CONTINUE
|
|
||||||
def chk_part(m):
|
|
||||||
for k,n in m.getnames():
|
|
||||||
if n and n.lower().endswith('.pif'):
|
|
||||||
self.sawpif = True
|
|
||||||
s = m.get_submsg()
|
|
||||||
print(m.get_content_type(),type(s),'modified:',m.ismodified())
|
|
||||||
if isinstance(s,email.message.Message):
|
|
||||||
return mime.check_attachments(s,chk_part)
|
|
||||||
return Milter.CONTINUE
|
|
||||||
|
|
||||||
with self.zf.open('virus7','r') as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
self.assertTrue(msg.ismultipart())
|
|
||||||
mime.check_attachments(msg,do_part)
|
|
||||||
self.assertTrue(('filename','application.pif') in names)
|
|
||||||
self.assertFalse(self.sawpif)
|
|
||||||
mime.check_attachments(msg,chk_part)
|
|
||||||
self.assertTrue(self.sawpif)
|
|
||||||
|
|
||||||
def testHTML(self,fname=""):
|
|
||||||
result = StringIO()
|
|
||||||
filter = mime.HTMLScriptFilter(result)
|
|
||||||
msg = """<! Illegal declaration used as comment>
|
|
||||||
<![if conditional]> Optional SGML <![endif]>
|
|
||||||
<!-- Legal SGML comment -->
|
|
||||||
"""
|
|
||||||
script = "<script lang=javascript> Dangerous script </script>"
|
|
||||||
filter.feed(msg + script)
|
|
||||||
filter.close()
|
|
||||||
#print(result.getvalue())
|
|
||||||
#print('---')
|
|
||||||
#print(msg + filter.msg)
|
|
||||||
self.assertTrue(result.getvalue() == msg + filter.msg)
|
|
||||||
|
|
||||||
def suite(): return unittest.makeSuite(MimeTestCase,'test')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
unittest.main()
|
|
||||||
else:
|
|
||||||
for fname in sys.argv[1:]:
|
|
||||||
with open(fname,'rb') as fp:
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
mime.defang(msg,scan_zip=True)
|
|
||||||
print(msg.as_string())
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
import unittest
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from Milter.policy import MTAPolicy
|
|
||||||
|
|
||||||
class Config(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.access_file='test/access.db'
|
|
||||||
self.access_file_nulls=True
|
|
||||||
self.access_file_colon = False
|
|
||||||
|
|
||||||
class PolicyTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.config = Config()
|
|
||||||
if os.access('test/access',os.R_OK):
|
|
||||||
if not os.path.exists('test/access.db') or \
|
|
||||||
os.path.getmtime('test/access') > os.path.getmtime('test/access.db'):
|
|
||||||
cmd = 'tr : ! <test/access | makemap hash test/access.db'
|
|
||||||
if os.system(cmd):
|
|
||||||
print('failed!')
|
|
||||||
else:
|
|
||||||
print("Missing test/access")
|
|
||||||
|
|
||||||
def testPolicy(self):
|
|
||||||
self.config.access_file_colon = False
|
|
||||||
self.config.access_file_nulls = True
|
|
||||||
with MTAPolicy('good@example.com',conf=self.config) as p:
|
|
||||||
pol = p.getPolicy('smtp-auth')
|
|
||||||
self.assertEqual(pol,'OK')
|
|
||||||
with MTAPolicy('bad@example.com',conf=self.config) as p:
|
|
||||||
pol = p.getPolicy('smtp-auth')
|
|
||||||
self.assertEqual(pol,'REJECT')
|
|
||||||
with MTAPolicy('bad@bad.example.com',conf=self.config) as p:
|
|
||||||
pol = p.getPolicy('smtp-auth')
|
|
||||||
self.assertEqual(pol,None)
|
|
||||||
with MTAPolicy('any@random.com',conf=self.config) as p:
|
|
||||||
pol = p.getPolicy('smtp-test')
|
|
||||||
self.assertEqual(pol,'REJECT')
|
|
||||||
with MTAPolicy('foo@bar.baz.com',conf=self.config) as p:
|
|
||||||
pol = p.getPolicy('smtp-test')
|
|
||||||
self.assertEqual(pol,'WILDCARD')
|
|
||||||
|
|
||||||
def suite(): return unittest.makeSuite(PolicyTestCase,'test')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
unittest.main()
|
|
||||||
else:
|
|
||||||
a = sys.argv[1:]
|
|
||||||
while len(a) >= 2:
|
|
||||||
e,k = a[:2]
|
|
||||||
with MTAPolicy(e,conf=Config()) as p:
|
|
||||||
pol = p.getPolicy(k)
|
|
||||||
print(pol)
|
|
||||||
a = a[2:]
|
|
||||||
-148
@@ -1,148 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import Milter
|
|
||||||
import sample
|
|
||||||
import template
|
|
||||||
import mime
|
|
||||||
import zipfile
|
|
||||||
from Milter.test import TestBase
|
|
||||||
from Milter.testctx import TestCtx
|
|
||||||
|
|
||||||
class TestMilter(TestBase,sample.sampleMilter):
|
|
||||||
def __init__(self):
|
|
||||||
TestBase.__init__(self)
|
|
||||||
sample.sampleMilter.__init__(self)
|
|
||||||
|
|
||||||
class BMSMilterTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.zf = zipfile.ZipFile('test/virus.zip','r')
|
|
||||||
self.zf.setpassword(b'denatured')
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.zf.close()
|
|
||||||
self.zf = None
|
|
||||||
|
|
||||||
def testTemplate(self,fname='test2'):
|
|
||||||
ctx = TestCtx()
|
|
||||||
Milter.factory = template.myMilter
|
|
||||||
ctx._setsymval('{auth_authen}','batman')
|
|
||||||
ctx._setsymval('{auth_type}','batcomputer')
|
|
||||||
ctx._setsymval('j','mailhost')
|
|
||||||
count = 10
|
|
||||||
while count > 0:
|
|
||||||
rc = ctx._connect(helo='milter-template.example.org')
|
|
||||||
self.assertEqual(rc,Milter.CONTINUE)
|
|
||||||
with open('test/'+fname,'rb') as fp:
|
|
||||||
rc = ctx._feedFile(fp)
|
|
||||||
milter = ctx.getpriv()
|
|
||||||
self.assertFalse(ctx._bodyreplaced,"Message body replaced")
|
|
||||||
ctx._close()
|
|
||||||
count -= 1
|
|
||||||
|
|
||||||
def testHeader(self,fname='utf8'):
|
|
||||||
ctx = TestCtx()
|
|
||||||
Milter.factory = sample.sampleMilter
|
|
||||||
ctx._setsymval('{auth_authen}','batman')
|
|
||||||
ctx._setsymval('{auth_type}','batcomputer')
|
|
||||||
ctx._setsymval('j','mailhost')
|
|
||||||
rc = ctx._connect()
|
|
||||||
self.assertEqual(rc,Milter.CONTINUE)
|
|
||||||
with open('test/'+fname,'rb') as fp:
|
|
||||||
rc = ctx._feedFile(fp)
|
|
||||||
milter = ctx.getpriv()
|
|
||||||
self.assertFalse(ctx._bodyreplaced,"Message body replaced")
|
|
||||||
fp = ctx._body
|
|
||||||
with open('test/'+fname+".tstout","wb") as ofp:
|
|
||||||
ofp.write(fp.getvalue())
|
|
||||||
ctx._close()
|
|
||||||
|
|
||||||
def testCtx(self,fname='virus1'):
|
|
||||||
ctx = TestCtx()
|
|
||||||
Milter.factory = sample.sampleMilter
|
|
||||||
ctx._setsymval('{auth_authen}','batman')
|
|
||||||
ctx._setsymval('{auth_type}','batcomputer')
|
|
||||||
ctx._setsymval('j','mailhost')
|
|
||||||
rc = ctx._connect()
|
|
||||||
self.assertTrue(rc == Milter.CONTINUE)
|
|
||||||
with self.zf.open(fname) as fp:
|
|
||||||
rc = ctx._feedFile(fp)
|
|
||||||
milter = ctx.getpriv()
|
|
||||||
# self.assertTrue(milter.user == 'batman',"getsymval failed: "+
|
|
||||||
# "%s != %s"%(milter.user,'batman'))
|
|
||||||
self.assertEqual(milter.user,'batman')
|
|
||||||
self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed")
|
|
||||||
self.assertTrue(rc == Milter.ACCEPT)
|
|
||||||
self.assertTrue(ctx._bodyreplaced,"Message body not replaced")
|
|
||||||
fp = ctx._body
|
|
||||||
with open('test/'+fname+".tstout","wb") as f:
|
|
||||||
f.write(fp.getvalue())
|
|
||||||
#self.assertTrue(fp.getvalue() == open("test/virus1.out","r").read())
|
|
||||||
fp.seek(0)
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
s = msg.get_payload(1).get_payload()
|
|
||||||
milter.log(s)
|
|
||||||
ctx._close()
|
|
||||||
|
|
||||||
def testDefang(self,fname='virus1'):
|
|
||||||
milter = TestMilter()
|
|
||||||
milter.setsymval('{auth_authen}','batman')
|
|
||||||
milter.setsymval('{auth_type}','batcomputer')
|
|
||||||
milter.setsymval('j','mailhost')
|
|
||||||
rc = milter.connect()
|
|
||||||
self.assertTrue(rc == Milter.CONTINUE)
|
|
||||||
with self.zf.open(fname) as fp:
|
|
||||||
rc = milter.feedFile(fp)
|
|
||||||
self.assertTrue(milter.user == 'batman',"getsymval failed")
|
|
||||||
# setsymlist not working in TestBase
|
|
||||||
#self.assertTrue(milter.auth_type != 'batcomputer',"setsymlist failed")
|
|
||||||
self.assertTrue(rc == Milter.ACCEPT)
|
|
||||||
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
|
|
||||||
fp = milter._body
|
|
||||||
with open('test/'+fname+".tstout","wb") as f:
|
|
||||||
f.write(fp.getvalue())
|
|
||||||
#self.assertTrue(fp.getvalue() == open("test/virus1.out","r").read())
|
|
||||||
fp.seek(0)
|
|
||||||
msg = mime.message_from_file(fp)
|
|
||||||
s = msg.get_payload(1).get_payload()
|
|
||||||
milter.log(s)
|
|
||||||
milter.close()
|
|
||||||
|
|
||||||
def testParse(self,fname='spam7'):
|
|
||||||
milter = TestMilter()
|
|
||||||
milter.connect('somehost')
|
|
||||||
rc = milter.feedMsg(fname)
|
|
||||||
self.assertTrue(rc == Milter.ACCEPT)
|
|
||||||
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.")
|
|
||||||
fp = milter._body
|
|
||||||
with open('test/'+fname+".tstout","wb") as f:
|
|
||||||
f.write(fp.getvalue())
|
|
||||||
milter.close()
|
|
||||||
|
|
||||||
def testDefang2(self):
|
|
||||||
milter = TestMilter()
|
|
||||||
milter.connect('somehost')
|
|
||||||
rc = milter.feedMsg('samp1')
|
|
||||||
self.assertTrue(rc == Milter.ACCEPT)
|
|
||||||
self.assertFalse(milter._bodyreplaced,"Milter needlessly replaced body.")
|
|
||||||
with self.zf.open("virus3") as fp:
|
|
||||||
rc = milter.feedFile(fp)
|
|
||||||
self.assertTrue(rc == Milter.ACCEPT)
|
|
||||||
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
|
|
||||||
fp = milter._body
|
|
||||||
with open("test/virus3.tstout","wb") as f:
|
|
||||||
f.write(fp.getvalue())
|
|
||||||
#self.assertTrue(fp.getvalue() == open("test/virus3.out","r").read())
|
|
||||||
with self.zf.open("virus6") as fp:
|
|
||||||
rc = milter.feedFile(fp)
|
|
||||||
self.assertTrue(rc == Milter.ACCEPT)
|
|
||||||
self.assertTrue(milter._bodyreplaced,"Message body not replaced")
|
|
||||||
self.assertTrue(milter._headerschanged,"Message headers not adjusted")
|
|
||||||
fp = milter._body
|
|
||||||
with open("test/virus6.tstout","wb") as f:
|
|
||||||
f.write(fp.getvalue())
|
|
||||||
milter.close()
|
|
||||||
|
|
||||||
def suite(): return unittest.makeSuite(BMSMilterTestCase,'test')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
import unittest
|
|
||||||
import doctest
|
|
||||||
import os
|
|
||||||
import Milter.utils
|
|
||||||
from Milter.cache import AddrCache
|
|
||||||
from Milter.dynip import is_dynip
|
|
||||||
from Milter.pyip6 import inet_ntop
|
|
||||||
|
|
||||||
class AddrCacheTestCase(unittest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.fname = 'test.dat'
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
if os.path.exists(self.fname):
|
|
||||||
os.remove(self.fname)
|
|
||||||
|
|
||||||
def testAdd(self):
|
|
||||||
cache = AddrCache(fname=self.fname)
|
|
||||||
cache['foo@bar.com'] = None
|
|
||||||
cache.addperm('baz@bar.com')
|
|
||||||
cache['temp@bar.com'] = 'testing'
|
|
||||||
self.assertTrue(cache.has_key('foo@bar.com'))
|
|
||||||
self.assertTrue(not cache.has_key('hello@bar.com'))
|
|
||||||
self.assertTrue('baz@bar.com' in cache)
|
|
||||||
self.assertEqual(cache['temp@bar.com'],'testing')
|
|
||||||
s = open(self.fname).readlines()
|
|
||||||
self.assertTrue(len(s) == 2)
|
|
||||||
self.assertTrue(s[0].startswith('foo@bar.com '))
|
|
||||||
self.assertEqual(s[1].strip(),'baz@bar.com')
|
|
||||||
# check that new result overrides old
|
|
||||||
cache['temp@bar.com'] = None
|
|
||||||
self.assertTrue(not cache['temp@bar.com'])
|
|
||||||
|
|
||||||
def testDomain(self):
|
|
||||||
with open(self.fname,'w') as fp:
|
|
||||||
print('spammer.com',file=fp)
|
|
||||||
cache = AddrCache(fname=self.fname)
|
|
||||||
cache.load(self.fname,30)
|
|
||||||
self.assertTrue('spammer.com' in cache)
|
|
||||||
|
|
||||||
def testParseHeader(self):
|
|
||||||
s='=?UTF-8?B?TGFzdCBGZXcgQ29sZHBsYXkgQWxidW0gQXJ0d29ya3MgQXZhaWxhYmxlAA?='
|
|
||||||
h = Milter.utils.parse_header(s)
|
|
||||||
self.assertEqual(h,'Last Few Coldplay Album Artworks Available\x00')
|
|
||||||
s='=?iso-8859-1?Q?Peter_=D8rum?= <orum@ditas.dk>'
|
|
||||||
h = Milter.utils.parse_header(s)
|
|
||||||
self.assertEqual(h,'Peter \xd8rum <orum@ditas.dk>')
|
|
||||||
|
|
||||||
@unittest.expectedFailure
|
|
||||||
def testParseAddress(self):
|
|
||||||
s = Milter.utils.parseaddr('a(WRONG)@b')
|
|
||||||
self.assertEqual(s,('WRONG', 'a@b'))
|
|
||||||
|
|
||||||
def suite():
|
|
||||||
s = unittest.makeSuite(AddrCacheTestCase,'test')
|
|
||||||
s.addTest(doctest.DocTestSuite(Milter.utils))
|
|
||||||
s.addTest(doctest.DocTestSuite(Milter.dynip))
|
|
||||||
s.addTest(doctest.DocTestSuite(Milter.pyip6))
|
|
||||||
return s
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.TextTestRunner().run(suite())
|
|
||||||
Reference in New Issue
Block a user