2 # set:encoding=iso-8859-1:
8 from email
import Message
, MIMEText
, MIMEMultipart
9 from email
.Iterators
import typed_subpart_iterator
, _structure
10 from smtpd
import PureProxy
20 TO_HOST
= 'olympe.madism.org'
24 ################################################################################
28 #-------------------------------------------------------------------------------
30 config
= ConfigParser
.ConfigParser()
31 config
.read(os
.path
.dirname(__file__
)+'/../configs/platal.conf')
33 def get_config(sec
,val
,default
=None):
35 return config
.get(sec
, val
)[1:-1]
36 except ConfigParser
.NoOptionError
, e
:
46 user
= get_config('Core', 'dbuser'),
47 passwd
= get_config('Core', 'dbpwd'),
48 unix_socket
='/var/run/mysqld/mysqld.sock')
52 def msg_of_str(data
): return email
.message_from_string(data
, _class
=BounceMessage
)
54 ################################################################################
58 #-------------------------------------------------------------------------------
60 class BounceMessage(Message
.Message
):
62 """this method returns the part that is commonely designed as the 'body'
64 for the multipart mails, we go into the first part that have non multipart childs, and then :
65 we return its first text/plain part if it exsists
66 else we return the first text/* part if it exists
67 else we return None else
69 for non multipart mails, we just return the current payload
71 if self
.is_multipart():
73 while _body
.get_payload(0).is_multipart():
74 _body
= _body
.get_payload(0)
77 for part
in typed_subpart_iterator(_body
):
78 if part
.get_content_subtype() == 'plain':
79 return part
.get_payload(decode
=True)
82 return buffer.get_payload(decode
=True)
83 return self
.get_payload(decode
=True)
85 def _qmail_attached_mail(self
):
86 """qmail is a dumb MTA that put the mail that has bounced RAW into the bounce message,
87 instead of making a traditionnal message/rfc822 attachement like any other MTA
89 it seems to be designed like this :
91 =============================================
93 --- Below this line is a copy of the message.
96 [rest of the embeded mail]
97 =============================================
99 so we just cut the qmail crap, and build a new message from the rest.
101 may DJB burn into coder's hell
103 msg
= self
.get_payload(decode
=True)
104 pos
= msg
.find("\n--- Below this line is a copy of the message.")
107 pos
= msg
.find("Return-Path:", pos
)
108 return msg_of_str(msg
[pos
:])
110 def attached_mail(self
):
111 """returns the attached mail that bounced, if it exists.
114 is the mail multipart ?
116 (1) return the first message/rfc822 part.
117 (2) return the first text/rfc822-headers part (AOHell)
118 (3) return None (may be a vacation + some disclaimer in attachment)
120 try to return the qmail-style embeded mail (but may be a vacation)
122 if self
.is_multipart():
123 for part
in typed_subpart_iterator(self
, 'message', 'rfc822'):
125 for part
in typed_subpart_iterator(self
, 'text', 'rfc822-headers'):
128 return self
._qmail_attached_mail()
130 def error_level(self
):
131 """determine the level of an error:
132 IGNORE == drop the mail
133 NOTICE == vacation, or any informative message we want to forward as is
134 ERROR == errors, that we want to handle
141 mysql
.execute ( "SELECT lvl,re,text FROM emails_bounces_re ORDER BY pos" )
142 nb
= int(mysql
.rowcount
)
143 for x
in range(0,nb
):
144 row
= mysql
.fetchone()
145 if re
.compile(str(row
[1]), re
.I | re
.M
).search(body
):
146 return (int(row
[0]), str(row
[2]))
150 def forge_error(self
, alias
, dest
, txt
):
151 """we have to do our little treatments for the broken mail,
152 and then we create an informative message for the original SENDER to :
153 - explain to him what happened (the detailed error)
154 - try to guess if the user may or may not have had the mail (by another leg)
155 - if no other leg, give an information to the SENDER on how he can give to us a real good leg
156 and attach any sensible information about the original mail (@see attached_mail)
159 mysql
.execute("SELECT id FROM aliases WHERE alias='%s' AND type IN ('alias', 'a_vie') LIMIT 1" %
(alias
))
160 if int(mysql
.rowcount
) is not 1:
162 uid
= mysql
.fetchone()[0]
163 mysql
.execute("UPDATE emails SET panne = NOW() WHERE uid='%s' AND email='%s'" %
(uid
, dest
))
164 mysql
.execute("REPLACE INTO emails_broken (uid,email) VALUES(%s, '%s')" %
(uid
, dest
))
165 mysql
.execute("""SELECT COUNT(*),
166 IFNULL(SUM(panne=0 OR (last!=0 AND ( TO_DAYS(NOW())-TO_DAYS(last) )>7 AND panne<last)), 0),
167 IFNULL(SUM(panne!=0 AND last!=0 AND ( TO_DAYS(NOW())-TO_DAYS(last) )<7 AND panne<last) , 0),
168 IFNULL(SUM(panne!=0 AND (last=0 OR ( TO_DAYS(NOW())-TO_DAYS(last) )<1)) , 0)
170 WHERE FIND_IN_SET('active', flags) AND uid=%s AND email!='%s'""" %
(uid
, dest
))
172 nb_act
, nb_ok
, nb_may
, nb_bad
= map(lambda x
: int(x
), mysql
.fetchone())
174 txt
= "Une des adresses de redirection de %s\n" %
(alias
) \
175 + "a généré une erreur (qui peut être temporaire) :\n" \
176 + "------------------------------------------------------------\n" \
178 + "------------------------------------------------------------\n\n"
180 if nb_ok
+ nb_may
is 0:
181 txt
+= "Toutes les adresses de redirection de ce correspondant\n" \
182 + "sont cassées à l'heure actuelle.\n\n" \
183 + "Prière de prévenir votre correspondant par d'autres moyens\n" \
184 + "pour lui signaler ce problème et qu'il puisse le corriger !!!"
186 txt
+= "Ce correspondant possède néanmoins %i autre(s) adresse(s) active(s)\n" %
(nb_may
) \
187 + "en erreur, mais ayant recu des mails dans les 7 derniers jours,\n" \
188 + "sans -- pour le moment -- avoir créé la moindre nouvelle erreur.\n\n" \
189 + "Ces adresses sont donc peut-être valides.\n"
191 txt
+= "Ce correspondant a en ce moment %i autre(s) adresse(s) valide(s).\n" %
(nb_ok
) \
192 + "Rien ne prouve cependant qu'elles étaient actives \n" \
193 + "au moment de l'envoi qui a échoué."
195 msg
= MIMEMultipart
.MIMEMultipart()
196 msg
['Subject'] = self
['Subject']
198 attach
= self
.attached_mail()
199 if attach
is not None:
200 txt
+= "\nCi-joint le mail dont la livraison a échoué\n"
201 msg
.attach(MIMEText
.MIMEText(txt
))
204 msg
.attach(MIMEText
.MIMEText(txt
))
208 def to_bounce(self
, alias
, dest
):
209 """this function returns a new Message, the one we really want to send.
211 alias holds one valide plat/al alias of the user
213 Case 0: the error is IGNORE : return None
214 Case 1: the error is NOTICE : we just return self
215 Case 2: we have a REAL error: use forge_error
217 lvl
, txt
= self
.error_level()
219 if lvl
is IGNORE
: return None
220 elif lvl
is NOTICE
: return self
221 elif lvl
is ERROR
: return self
.forge_error(alias
, dest
, txt
)
225 class BounceProxy(PureProxy
):
226 def __init__(self
, localaddr
, remoteaddr
):
227 PureProxy
.__init__(self
, localaddr
, remoteaddr
)
228 self
._rcpt_re
= re
.compile(r
'^([^_]*)__(.*)__([^_+=]*)\+(.*)=([^_+=]*)@bounces.m4x.org$')
231 def process_rcpt(self
, rcpttos
):
233 m
= self
._rcpt_re
.match(to
)
234 if m
is None: continue
235 return ( m
.group(1), m
.group(2)+'@'+m
.group(3), m
.group(4)+'@'+m
.group(5) )
239 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
241 alias
, sender
, dest
= self
.process_rcpt(rcpttos
)
242 bounce
= msg_of_str(data
).to_bounce(alias
, dest
)
243 if bounce
is not None:
244 bounce
['From'] = """"Serveur de courier Polytechnique.org" <MAILER-DAEMON@bounces.m4x.org>"""
245 bounce
['To'] = sender
246 self
._deliver("MAILER-DAEMON@bounces.m4x.org", [sender
], bounce
.as_string())
249 # SPAM or broken msg, we just drop it
253 ################################################################################
257 #-------------------------------------------------------------------------------
260 Proxy
= BounceProxy(('127.0.0.1', FROM_PORT
), (TO_HOST
, TO_PORT
))