source: trunk/email2trac.py.in @ 208

Last change on this file since 208 was 208, checked in by bas, 16 years ago

email2trac.py.in:

  • implemented black_list option, closes #39
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 30.2 KB
Line 
1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
22"""
23email2trac.py -- Email tickets to Trac.
24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
30Changed By: Bas van der Vlies <basv@sara.nl>
31Date      : 13 September 2005
32Descr.    : Added config file and command line options, spam level
33            detection, reply address and mailto option. Unicode support
34
35Changed By: Walter de Jong <walter@sara.nl>
36Descr.    : multipart-message code and trac attachments
37
38
39The scripts reads emails from stdin and inserts directly into a Trac database.
40MIME headers are mapped as follows:
41
42        * From:      => Reporter
43                     => CC (Optional via reply_all option)
44        * Subject:   => Summary
45        * Body       => Description
46        * Component  => Can be set to SPAM via spam_level option
47
48How to use
49----------
50 * Create an config file:
51        [DEFAULT]                      # REQUIRED
52        project      : /data/trac/test # REQUIRED
53        debug        : 1               # OPTIONAL, if set print some DEBUG info
54        spam_level   : 4               # OPTIONAL, if set check for SPAM mail
55        reply_all    : 1               # OPTIONAL, if set then fill in ticket CC field
56        umask        : 022             # OPTIONAL, if set then use this umask for creation of the attachments
57        mailto_link  : 1               # OPTIONAL, if set then [mailto:<>] in description
58        mailto_cc    : basv@sara.nl    # OPTIONAL, use this address as CC in mailto line
59        ticket_update: 1               # OPTIONAL, if set then check if this is an update for a ticket
60        trac_version : 0.9             # OPTIONAL, default is 0.10
61
62        [jouvin]                       # OPTIONAL project declaration, if set both fields necessary
63        project      : /data/trac/jouvin # use -p|--project jouvin. 
64       
65 * default config file is : /etc/email2trac.conf
66
67 * Commandline opions:
68                -h,--help
69                -f,--file  <configuration file>
70                -n,--dry-run
71                -p, --project <project name>
72                -t, --ticket_prefix <name>
73
74SVN Info:
75        $Id: email2trac.py.in 208 2008-05-30 07:58:55Z bas $
76"""
77import os
78import sys
79import string
80import getopt
81import stat
82import time
83import email
84import email.Iterators
85import email.Header
86import re
87import urllib
88import unicodedata
89import ConfigParser
90from stat import *
91import mimetypes
92import traceback
93
94
95# Will fail where unavailable, e.g. Windows
96#
97try:
98    import syslog
99    SYSLOG_AVAILABLE = True
100except ImportError:
101    SYSLOG_AVAILABLE = False
102
103from datetime import tzinfo, timedelta, datetime
104from trac import config as trac_config
105
106# Some global variables
107#
108trac_default_version = '0.10'
109m = None
110
111# A UTC class needed for trac version 0.11, added by
112# tbaschak at ktc dot mb dot ca
113#
114class UTC(tzinfo):
115        """UTC"""
116        ZERO = timedelta(0)
117        HOUR = timedelta(hours=1)
118       
119        def utcoffset(self, dt):
120                return self.ZERO
121               
122        def tzname(self, dt):
123                return "UTC"
124               
125        def dst(self, dt):
126                return self.ZERO
127
128
129class TicketEmailParser(object):
130        env = None
131        comment = '> '
132   
133        def __init__(self, env, parameters, version):
134                self.env = env
135
136                # Database connection
137                #
138                self.db = None
139
140                # Save parameters
141                #
142                self.parameters = parameters
143
144                # Some useful mail constants
145                #
146                self.author = None
147                self.email_addr = None
148                self.email_from = None
149
150                self.VERSION = version
151                self.DRY_RUN = parameters['dry_run']
152
153                self.get_config = self.env.config.get
154
155                if parameters.has_key('umask'):
156                        os.umask(int(parameters['umask'], 8))
157
158                if parameters.has_key('debug'):
159                        self.DEBUG = int(parameters['debug'])
160                else:
161                        self.DEBUG = 0
162
163                if parameters.has_key('mailto_link'):
164                        self.MAILTO = int(parameters['mailto_link'])
165                        if parameters.has_key('mailto_cc'):
166                                self.MAILTO_CC = parameters['mailto_cc']
167                        else:
168                                self.MAILTO_CC = ''
169                else:
170                        self.MAILTO = 0
171
172                if parameters.has_key('spam_level'):
173                        self.SPAM_LEVEL = int(parameters['spam_level'])
174                else:
175                        self.SPAM_LEVEL = 0
176
177                if parameters.has_key('spam_header'):
178                        self.SPAM_HEADER = parameters['spam_header']
179                else:
180                        self.SPAM_HEADER = 'X-Spam-Score'
181
182                if parameters.has_key('email_quote'):
183                        self.EMAIL_QUOTE = str(parameters['email_quote'])
184                else:   
185                        self.EMAIL_QUOTE = '> '
186
187                if parameters.has_key('email_header'):
188                        self.EMAIL_HEADER = int(parameters['email_header'])
189                else:
190                        self.EMAIL_HEADER = 0
191
192                if parameters.has_key('alternate_notify_template'):
193                        self.notify_template = str(parameters['alternate_notify_template'])
194                else:
195                        self.notify_template = None
196
197                if parameters.has_key('reply_all'):
198                        self.REPLY_ALL = int(parameters['reply_all'])
199                else:
200                        self.REPLY_ALL = 0
201
202                if parameters.has_key('ticket_update'):
203                        self.TICKET_UPDATE = int(parameters['ticket_update'])
204                else:
205                        self.TICKET_UPDATE = 0
206
207                if parameters.has_key('drop_spam'):
208                        self.DROP_SPAM = int(parameters['drop_spam'])
209                else:
210                        self.DROP_SPAM = 0
211
212                if parameters.has_key('verbatim_format'):
213                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
214                else:
215                        self.VERBATIM_FORMAT = 1
216
217                if parameters.has_key('strip_signature'):
218                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
219                else:
220                        self.STRIP_SIGNATURE = 0
221
222                if parameters.has_key('strip_quotes'):
223                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
224                else:
225                        self.STRIP_QUOTES = 0
226
227                if parameters.has_key('use_textwrap'):
228                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
229                else:
230                        self.USE_TEXTWRAP = 0
231
232                if parameters.has_key('python_egg_cache'):
233                        self.python_egg_cache = str(parameters['python_egg_cache'])
234                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
235
236                # Use OS independend functions
237                #
238                self.TMPDIR = os.path.normcase('/tmp')
239                if parameters.has_key('tmpdir'):
240                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
241
242                if parameters.has_key('ignore_trac_user_settings'):
243                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
244                else:
245                        self.IGNORE_TRAC_USER_SETTINGS = 0
246
247        def spam(self, message):
248                """
249                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
250                # Note if Spam_level then '*' are included
251                """
252                spam = False
253                if message.has_key(self.SPAM_HEADER):
254                        spam_l = string.split(message[self.SPAM_HEADER])
255
256                        try:
257                                number = spam_l[0].count('*')
258                        except IndexError, detail:
259                                number = 0
260                               
261                        if number >= self.SPAM_LEVEL:
262                                spam = True
263                               
264                # treat virus mails as spam
265                #
266                elif message.has_key('X-Virus-found'):                 
267                        spam = True
268
269                # How to handle SPAM messages
270                #
271                if self.DROP_SPAM and spam:
272                        if self.DEBUG > 2 :
273                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
274
275                        return 'drop'   
276
277                elif spam:
278
279                        return 'Spam'   
280
281                else:
282
283                        return False
284
285
286        def blacklisted_from(self):
287                """
288                This function wil check if the email address is in the black list
289                """
290                try:
291                        black_list = self.parameters['black_list']
292                except KeyError, detail:
293                        black_list = 'MAILER-DAEMON@'
294
295                black_list = string.split(black_list)   
296                if not black_list.count('MAILER-DAEMON@'):
297                        black_list.append('MAILER-DAEMON@')
298
299                for entry in black_list:       
300                        FROM_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
301                        result =  FROM_RE.search(self.email_addr)
302                        if result:
303                                return True
304
305                return False
306
307        def email_to_unicode(self, message_str):
308                """
309                Email has 7 bit ASCII code, convert it to unicode with the charset
310        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
311                understands it.
312                """
313                results =  email.Header.decode_header(message_str)
314                str = None
315                for text,format in results:
316                        if format:
317                                try:
318                                        temp = unicode(text, format)
319                                except UnicodeError, detail:
320                                        # This always works
321                                        #
322                                        temp = unicode(text, 'iso-8859-15')
323                                except LookupError, detail:
324                                        #text = 'ERROR: Could not find charset: %s, please install' %format
325                                        #temp = unicode(text, 'iso-8859-15')
326                                        temp = message_str
327                                       
328                        else:
329                                temp = string.strip(text)
330                                temp = unicode(text, 'iso-8859-15')
331
332                        if str:
333                                str = '%s %s' %(str, temp)
334                        else:
335                                str = '%s' %temp
336
337                #str = str.encode('utf-8')
338                return str
339
340        def debug_attachments(self, message):
341                n = 0
342                for part in message.walk():
343                        if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
344                                print 'TD: multipart container'
345                                continue
346
347                        n = n + 1
348                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
349                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
350
351                        if part.is_multipart():
352                                print 'TD: this part is multipart'
353                                payload = part.get_payload(decode=1)
354                                print 'TD: payload:', payload
355                        else:
356                                print 'TD: this part is not multipart'
357
358                        file = 'part%d' %n
359                        part_file = os.path.join(self.TMPDIR, file)
360                        #part_file = '/var/tmp/part%d' % n
361                        print 'TD: writing part%d (%s)' % (n,part_file)
362                        fx = open(part_file, 'wb')
363                        text = part.get_payload(decode=1)
364                        if not text:
365                                text = '(None)'
366                        fx.write(text)
367                        fx.close()
368                        try:
369                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
370                        except OSError:
371                                pass
372
373        def email_header_txt(self, m):
374                """
375                Display To and CC addresses in description field
376                """
377                str = ''
378                if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
379                        str = "'''To:''' %s [[BR]]" %(m['To'])
380                if m['Cc'] and len(m['Cc']) > 0:
381                        str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc'])
382
383                return  self.email_to_unicode(str)
384
385
386        def set_owner(self, ticket):
387                """
388                Select default owner for ticket component
389                """
390                #### return self.get_config('ticket', 'default_component')
391                cursor = self.db.cursor()
392                sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
393                cursor.execute(sql)
394                try:
395                        ticket['owner'] = cursor.fetchone()[0]
396                except TypeError, detail:
397                        ticket['owner'] = None
398
399        def get_sender_info(self, message):
400                """
401                Get the default author name and email address from the message
402                """
403
404                self.email_from = self.email_to_unicode(message['from'])
405                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
406
407                # Maybe for later user
408                #self.email_from =  self.email_to_unicode(self.email_addr)
409
410
411                if self.IGNORE_TRAC_USER_SETTINGS:
412                        return
413
414                # Is this a registered user, use email address as search key:
415                # result:
416                #   u : login name
417                #   n : Name that the user has set in the settings tab
418                #   e : email address that the user has set in the settings tab
419                #
420                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
421                                if e == self.email_addr ]
422
423                if len(users) == 1:
424                        self.email_from = users[0][0]
425
426        def set_reply_fields(self, ticket, message):
427                """
428                Set all the right fields for a new ticket
429                """
430                ticket['reporter'] = self.email_from
431
432                # Put all CC-addresses in ticket CC field
433                #
434                if self.REPLY_ALL:
435                        #tos = message.get_all('to', [])
436                        ccs = message.get_all('cc', [])
437
438                        addrs = email.Utils.getaddresses(ccs)
439                        if not addrs:
440                                return
441
442                        # Remove reporter email address if notification is
443                        # on
444                        #
445                        if self.notification:
446                                try:
447                                        addrs.remove((self.author, self.email_addr))
448                                except ValueError, detail:
449                                        pass
450
451                        for name,mail in addrs:
452                                try:
453                                        mail_list = '%s, %s' %(mail_list, mail)
454                                except:
455                                        mail_list = mail
456
457                        if mail_list:
458                                ticket['cc'] = self.email_to_unicode(mail_list)
459
460        def save_email_for_debug(self, message, tempfile=False):
461                if tempfile:
462                        import tempfile
463                        msg_file = tempfile.mktemp('.email2trac')
464                else:
465                        #msg_file = '/var/tmp/msg.txt'
466                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
467
468                print 'TD: saving email to %s' % msg_file
469                fx = open(msg_file, 'wb')
470                fx.write('%s' % message)
471                fx.close()
472                try:
473                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
474                except OSError:
475                        pass
476
477        def str_to_dict(self, str):
478                """
479                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
480                """
481                # Skip the last ':' character
482                #
483                fields = string.split(str[:-1], ',')
484
485                result = dict()
486                for field in fields:
487                        try:
488                                index, value = string.split(field,'=')
489
490                                # We can not change the description of a ticket via the subject
491                                # line. The description is the body of the email
492                                #
493                                if index.lower() in ['description']:
494                                        continue
495
496                                if value:
497                                        result[index.lower()] = value
498
499                        except ValueError:
500                                pass
501
502                return result
503
504        def update_ticket_fields(self, ticket, user_dict, use_default=None):
505                """
506                This will update the ticket fields. It will check if the
507                given fields are known and if the right values are specified
508                It will only update the ticket field value:
509                        - If the field is known
510                        - If the value supplied is valid for the ticket field.
511                          If not then there are two options:
512                           1) Skip the value (use_default=None)
513                           2) Set default value for field (use_default=1)
514                """
515
516                # Build a system dictionary from the ticket fields
517                # with field as index and option as value
518                #
519                sys_dict = dict()
520                for field in ticket.fields:
521                        try:
522                                sys_dict[field['name']] = field['options']
523
524                        except KeyError:
525                                sys_dict[field['name']] = None
526                                pass
527
528                # Check user supplied fields an compare them with the
529                # system one's
530                #
531                for field,value in user_dict.items():
532                        if self.DEBUG >= 10:
533                                print  'user_field\t %s = %s' %(field,value)
534
535                        if sys_dict.has_key(field):
536
537                                # Check if value is an allowed system option, if TypeError then
538                                # every value is allowed
539                                #
540                                try:
541                                        if value in sys_dict[field]:
542                                                ticket[field] = value
543                                        else:
544                                                # Must we set a default if value is not allowed
545                                                #
546                                                if use_default:
547                                                        value = self.get_config('ticket', 'default_%s' %(field) )
548                                                        ticket[field] = value
549
550                                except TypeError:
551                                        ticket[field] = value
552
553                                if self.DEBUG >= 10:
554                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
555                                       
556        def ticket_update(self, m, spam):
557                """
558                If the current email is a reply to an existing ticket, this function
559                will append the contents of this email to that ticket, instead of
560                creating a new one.
561                """
562
563                if not m['Subject']:
564                        return False
565                else:
566                        subject  = self.email_to_unicode(m['Subject'])
567
568                # [hic] #1529: Re: LRZ
569                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
570                #
571                TICKET_RE = re.compile(r"""
572                                        (?P<ticketnr>[#][0-9]+:)
573                                        |(?P<ticketnr_fields>[#][\d]+\?.*?:)
574                                        """, re.VERBOSE)
575
576                result =  TICKET_RE.search(subject)
577                if not result:
578                        return False
579
580                # Must we update ticket fields
581                #
582                update_tkt_fields = dict()
583                try:
584                        nr, keywords = string.split(result.group('ticketnr_fields'), '?')
585                        update_tkt_fields = self.str_to_dict(keywords)
586
587                        # Strip '#'
588                        #
589                        ticket_id = int(nr[1:])
590
591                except AttributeError:
592                        # Strip '#' and ':'
593                        #
594                        nr = result.group('ticketnr')
595                        ticket_id = int(nr[1:-1])
596
597
598                # When is the change committed
599                #
600                #
601                if self.VERSION == 0.11:
602                        utc = UTC()
603                        when = datetime.now(utc)
604                else:
605                        when = int(time.time())
606
607                try:
608                        tkt = Ticket(self.env, ticket_id, self.db)
609                except util.TracError, detail:
610                        return False
611
612                # Must we update some ticket fields properties
613                #
614                if update_tkt_fields:
615                        self.update_ticket_fields(tkt, update_tkt_fields)
616
617                body_text = self.get_body_text(m)
618                if self.EMAIL_HEADER:
619                        head = self.email_header_txt(m)
620                        body_text = u"\r\n%s \r\n%s" %(head, body_text)
621
622                #if self.MAILTO:
623                #       mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
624                #       body_text = u"\r\n%s \r\n%s" %(mailto, body_text)
625
626                tkt.save_changes(self.author, body_text, when)
627                tkt['id'] = ticket_id
628
629                if self.VERSION  == 0.9:
630                        str = self.attachments(m, tkt, True)
631                else:
632                        str = self.attachments(m, tkt)
633
634                if self.notification and not spam:
635                        self.notify(tkt, False, when)
636
637                return True
638
639        def set_ticket_fields(self, ticket):
640                """
641                set the ticket fields to value specified
642                        - /etc/email2trac.conf with <prefix>_<field>
643                        - trac default values, trac.ini
644                """
645                user_dict = dict()
646
647                for field in ticket.fields:
648
649                        name = field['name']
650
651                        # default trac value
652                        #
653                        value = self.get_config('ticket', 'default_%s' %(name) )
654                        if self.DEBUG > 10:
655                                print 'trac.ini name %s = %s' %(name, value)
656
657                        prefix = self.parameters['ticket_prefix']
658                        try:
659                                value = self.parameters['%s_%s' %(prefix, name)]
660                                if self.DEBUG > 10:
661                                        print 'email2trac.conf %s = %s ' %(name, value)
662
663                        except KeyError, detail:
664                                pass
665               
666                        if self.DEBUG:
667                                print 'user_dict[%s] = %s' %(name, value)
668
669                        user_dict[name] = value
670
671                self.update_ticket_fields(ticket, user_dict, use_default=1)
672
673                # Set status ticket
674                #`
675                ticket['status'] = 'new'
676
677
678
679        def new_ticket(self, msg, spam):
680                """
681                Create a new ticket
682                """
683                tkt = Ticket(self.env)
684
685                self.set_ticket_fields(tkt)
686
687                # Some defaults
688                #
689                #tkt['status'] = 'new'
690                #tkt['milestone'] = self.get_config('ticket', 'default_milestone')
691                #tkt['priority'] = self.get_config('ticket', 'default_priority')
692                #tkt['severity'] = self.get_config('ticket', 'default_severity')
693                #tkt['version'] = self.get_config('ticket', 'default_version')
694                #tkt['type'] = self.get_config('ticket', 'default_type')
695
696                # Old style setting for component, will be removed
697                #
698                if spam:
699                        tkt['component'] = 'Spam'
700
701                elif self.parameters.has_key('component'):
702                        tkt['component'] = self.parameters['component']
703
704                if not msg['Subject']:
705                        tkt['summary'] = u'(No subject)'
706                else:
707                        tkt['summary'] = self.email_to_unicode(msg['Subject'])
708
709
710                self.set_reply_fields(tkt, msg)
711
712                # produce e-mail like header
713                #
714                head = ''
715                if self.EMAIL_HEADER > 0:
716                        head = self.email_header_txt(msg)
717                       
718                body_text = self.get_body_text(msg)
719
720                tkt['description'] = '\r\n%s\r\n%s' \
721                        %(head, body_text)
722
723                #when = int(time.time())
724                #
725                utc = UTC()
726                when = datetime.now(utc)
727
728                if self.DRY_RUN:
729                        ticket_id = 'DRY_RUN'
730                else:
731                        ticket_id = tkt.insert()
732                       
733                tkt['id'] = ticket_id
734
735                changed = False
736                comment = ''
737
738                # Rewrite the description if we have mailto enabled
739                #
740                if self.MAILTO:
741                        changed = True
742                        comment = u'\nadded mailto line\n'
743                        mailto = self.html_mailto_link(tkt['summary'], ticket_id, body_text)
744                        tkt['description'] = u'\r\n%s\r\n%s%s\r\n' \
745                                %(head, mailto, body_text)
746
747                str =  self.attachments(msg, tkt)
748                if str:
749                        changed = True
750                        comment = '%s\n%s\n' %(comment, str)
751
752                if changed:
753                        if self.DRY_RUN:
754                                print 'DRY_RUN: tkt.save_changes(self.author, comment)'
755                        else:
756                                tkt.save_changes(self.author, comment)
757                                #print tkt.get_changelog(self.db, when)
758
759                if self.notification:
760                        if self.DRY_RUN:
761                                print 'DRY_RUN: self.notify(tkt, True)'
762                        else:
763                                if not spam:
764                                        self.notify(tkt, True)
765                                #self.notify(tkt, False)
766
767        def parse(self, fp):
768                global m
769
770                m = email.message_from_file(fp)
771                if not m:
772                        return
773
774                if self.DEBUG > 1:        # save the entire e-mail message text
775                        self.save_email_for_debug(m)
776                        self.debug_attachments(m)
777
778                self.db = self.env.get_db_cnx()
779                self.get_sender_info(m)
780
781                if self.blacklisted_from():
782                        if self.DEBUG > 1 :
783                                print 'Message rejected : From: in blacklist'
784                        return False
785
786                # If drop the message
787                #
788                if self.spam(m) == 'drop':
789                        return False
790
791                elif self.spam(m) == 'spam':
792                        spam_msg = True
793
794                else:
795                        spam_msg = False
796
797                if self.get_config('notification', 'smtp_enabled') in ['true']:
798                        self.notification = 1
799                else:
800                        self.notification = 0
801
802                # Must we update existing tickets
803                #
804                if self.TICKET_UPDATE > 0:
805                        if self.ticket_update(m, spam_msg):
806                                return True
807
808                self.new_ticket(m, spam_msg)
809
810        def strip_signature(self, text):
811                """
812                Strip signature from message, inspired by Mailman software
813                """
814                body = []
815                for line in text.splitlines():
816                        if line == '-- ':
817                                break
818                        body.append(line)
819
820                return ('\n'.join(body))
821
822        def strip_quotes(self, text):
823                """
824                Strip quotes from message by Nicolas Mendoza
825                """
826                body = []
827                for line in text.splitlines():
828                        if line.startswith(self.EMAIL_QUOTE):
829                                continue
830                        body.append(line)
831
832                return ('\n'.join(body))
833
834        def wrap_text(self, text, replace_whitespace = False):
835                """
836                Will break a lines longer then given length into several small
837                lines of size given length
838                """
839                import textwrap
840
841                LINESEPARATOR = '\n'
842                reformat = ''
843
844                for s in text.split(LINESEPARATOR):
845                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
846                        if tmp:
847                                reformat = '%s\n%s' %(reformat,tmp)
848                        else:
849                                reformat = '%s\n' %reformat
850
851                return reformat
852
853                # Python2.4 and higher
854                #
855                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
856                #
857
858
859        def get_body_text(self, msg):
860                """
861                put the message text in the ticket description or in the changes field.
862                message text can be plain text or html or something else
863                """
864                has_description = 0
865                encoding = True
866                ubody_text = u'No plain text message'
867                for part in msg.walk():
868
869                        # 'multipart/*' is a container for multipart messages
870                        #
871                        if part.get_content_maintype() == 'multipart':
872                                continue
873
874                        if part.get_content_type() == 'text/plain':
875                                # Try to decode, if fails then do not decode
876                                #
877                                body_text = part.get_payload(decode=1)
878                                if not body_text:                       
879                                        body_text = part.get_payload(decode=0)
880       
881                                if self.STRIP_SIGNATURE:
882                                        body_text = self.strip_signature(body_text)
883
884                                if self.STRIP_QUOTES:
885                                        body_text = self.strip_quotes(body_text)
886
887                                if self.USE_TEXTWRAP:
888                                        body_text = self.wrap_text(body_text)
889
890                                # Get contents charset (iso-8859-15 if not defined in mail headers)
891                                #
892                                charset = part.get_content_charset()
893                                if not charset:
894                                        charset = 'iso-8859-15'
895
896                                try:
897                                        ubody_text = unicode(body_text, charset)
898
899                                except UnicodeError, detail:
900                                        ubody_text = unicode(body_text, 'iso-8859-15')
901
902                                except LookupError, detail:
903                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
904
905                        elif part.get_content_type() == 'text/html':
906                                ubody_text = '(see attachment for HTML mail message)'
907
908                        else:
909                                ubody_text = '(see attachment for message)'
910
911                        has_description = 1
912                        break           # we have the description, so break
913
914                if not has_description:
915                        ubody_text = '(see attachment for message)'
916
917                # A patch so that the web-interface will not update the description
918                # field of a ticket
919                #
920                ubody_text = ('\r\n'.join(ubody_text.splitlines()))
921
922                #  If we can unicode it try to encode it for trac
923                #  else we a lot of garbage
924                #
925                #if encoding:
926                #       ubody_text = ubody_text.encode('utf-8')
927
928                if self.VERBATIM_FORMAT:
929                        ubody_text = '{{{\r\n%s\r\n}}}' %ubody_text
930                else:
931                        ubody_text = '%s' %ubody_text
932
933                return ubody_text
934
935        def notify(self, tkt , new=True, modtime=0):
936                """
937                A wrapper for the TRAC notify function. So we can use templates
938                """
939                try:
940                        # create false {abs_}href properties, to trick Notify()
941                        #
942                        if not self.VERSION == 0.11:
943                                self.env.abs_href = Href(self.get_config('project', 'url'))
944                                self.env.href = Href(self.get_config('project', 'url'))
945
946                        tn = TicketNotifyEmail(self.env)
947                        if self.notify_template:
948                                tn.template_name = self.notify_template;
949
950                        tn.notify(tkt, new, modtime)
951
952                except Exception, e:
953                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
954
955        def html_mailto_link(self, subject, id, body):
956                if not self.author:
957                        author = self.email_addr
958                else:   
959                        author = self.author
960
961                # Must find a fix
962                #
963                #arr = string.split(body, '\n')
964                #arr = map(self.mail_line, arr)
965                #body = string.join(arr, '\n')
966                #body = '%s wrote:\n%s' %(author, body)
967
968                # Temporary fix
969                #
970                str = 'mailto:%s?Subject=%s&Cc=%s' %(
971                       urllib.quote(self.email_addr),
972                           urllib.quote('Re: #%s: %s' %(id, subject)),
973                           urllib.quote(self.MAILTO_CC)
974                           )
975
976                str = '\r\n{{{\r\n#!html\r\n<a href="%s">Reply to: %s</a>\r\n}}}\r\n' %(str, author)
977                return str
978
979        def attachments(self, message, ticket, update=False):
980                '''
981                save any attachments as files in the ticket's directory
982                '''
983                count = 0
984                first = 0
985                number = 0
986
987                # Get Maxium attachment size
988                #
989                max_size = int(self.get_config('attachment', 'max_size'))
990                status   = ''
991
992                for part in message.walk():
993                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
994                                continue
995
996                        if not first:                                                                           # first content is the message
997                                first = 1
998                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
999                                        continue
1000
1001                        filename = part.get_filename()
1002                        if not filename:
1003                                number = number + 1
1004                                filename = 'part%04d' % number
1005
1006                                ext = mimetypes.guess_extension(part.get_content_type())
1007                                if not ext:
1008                                        ext = '.bin'
1009
1010                                filename = '%s%s' % (filename, ext)
1011                        else:
1012                                filename = self.email_to_unicode(filename)
1013
1014                        # From the trac code
1015                        #
1016                        filename = filename.replace('\\', '/').replace(':', '/')
1017                        filename = os.path.basename(filename)
1018
1019                        # We try to normalize the filename to utf-8 NFC if we can.
1020                        # Files uploaded from OS X might be in NFD.
1021                        # Check python version and then try it
1022                        #
1023                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1024                                try:
1025                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1026                                except TypeError:
1027                                        pass
1028
1029                        url_filename = urllib.quote(filename)
1030                        #
1031                        # Must be tuneables HvB
1032                        #
1033                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, url_filename))
1034                        text = part.get_payload(decode=1)
1035                        if not text:
1036                                text = '(None)'
1037                        fd.write(text)
1038                        fd.close()
1039
1040                        # get the file_size
1041                        #
1042                        stats = os.lstat(path)
1043                        file_size = stats[stat.ST_SIZE]
1044
1045                        # Check if the attachment size is allowed
1046                        #
1047                        if (max_size != -1) and (file_size > max_size):
1048                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1049                                        %(status, filename, file_size, max_size)
1050
1051                                os.unlink(path)
1052                                continue
1053                        else:
1054                                count = count + 1
1055                                       
1056                        # Insert the attachment
1057                        #
1058                        fd = open(path)
1059                        att = attachment.Attachment(self.env, 'ticket', ticket['id'])
1060
1061                        # This will break the ticket_update system, the body_text is vaporized
1062                        # ;-(
1063                        #
1064                        if not update:
1065                                att.author = self.author
1066                                att.description = self.email_to_unicode('Added by email2trac')
1067
1068                        att.insert(url_filename, fd, file_size)
1069                        #except  util.TracError, detail:
1070                        #       print detail
1071
1072                        # Remove the created temporary filename
1073                        #
1074                        fd.close()
1075                        os.unlink(path)
1076
1077                # Return how many attachments
1078                #
1079                status = 'This message has %d attachment(s)\n%s' %(count, status)
1080                return status
1081
1082
1083def mkdir_p(dir, mode):
1084        '''do a mkdir -p'''
1085
1086        arr = string.split(dir, '/')
1087        path = ''
1088        for part in arr:
1089                path = '%s/%s' % (path, part)
1090                try:
1091                        stats = os.stat(path)
1092                except OSError:
1093                        os.mkdir(path, mode)
1094
1095def ReadConfig(file, name):
1096        """
1097        Parse the config file
1098        """
1099
1100        if not os.path.isfile(file):
1101                print 'File %s does not exist' %file
1102                sys.exit(1)
1103
1104        config = trac_config.Configuration(file)
1105
1106        # Use given project name else use defaults
1107        #
1108        if name:
1109                sections = config.sections()
1110                if not name in sections:
1111                        print "Not a valid project name: %s" %name
1112                        print "Valid names: %s" %sections
1113                        sys.exit(1)
1114
1115                project =  dict()
1116                for option, value in  config.options(name):
1117                        project[option] = value
1118
1119        else:
1120                project = config.defaults()
1121
1122        return project
1123
1124
1125if __name__ == '__main__':
1126        # Default config file
1127        #
1128        configfile = '@email2trac_conf@'
1129        project = ''
1130        component = ''
1131        ticket_prefix = 'default'
1132        dry_run = None
1133
1134        ENABLE_SYSLOG = 0
1135
1136
1137        SHORT_OPT = 'chf:np:t:'
1138        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
1139
1140        try:
1141                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1142        except getopt.error,detail:
1143                print __doc__
1144                print detail
1145                sys.exit(1)
1146       
1147        project_name = None
1148        for opt,value in opts:
1149                if opt in [ '-h', '--help']:
1150                        print __doc__
1151                        sys.exit(0)
1152                elif opt in ['-c', '--component']:
1153                        component = value
1154                elif opt in ['-f', '--file']:
1155                        configfile = value
1156                elif opt in ['-n', '--dry-run']:
1157                        dry_run = True
1158                elif opt in ['-p', '--project']:
1159                        project_name = value
1160                elif opt in ['-t', '--ticket_prefix']:
1161                        ticket_prefix = value
1162       
1163        settings = ReadConfig(configfile, project_name)
1164        if not settings.has_key('project'):
1165                print __doc__
1166                print 'No Trac project is defined in the email2trac config file.'
1167                sys.exit(1)
1168       
1169        if component:
1170                settings['component'] = component
1171
1172        # The default prefix for ticket values in email2trac.conf
1173        #
1174        settings['ticket_prefix'] = ticket_prefix
1175        settings['dry_run'] = dry_run
1176       
1177        if settings.has_key('trac_version'):
1178                version = settings['trac_version']
1179        else:
1180                version = trac_default_version
1181
1182
1183        #debug HvB
1184        #print settings
1185
1186        try:
1187                if version == '0.9':
1188                        from trac import attachment
1189                        from trac.env import Environment
1190                        from trac.ticket import Ticket
1191                        from trac.web.href import Href
1192                        from trac import util
1193                        from trac.Notify import TicketNotifyEmail
1194                elif version == '0.10':
1195                        from trac import attachment
1196                        from trac.env import Environment
1197                        from trac.ticket import Ticket
1198                        from trac.web.href import Href
1199                        from trac import util
1200                        #
1201                        # return  util.text.to_unicode(str)
1202                        #
1203                        # see http://projects.edgewall.com/trac/changeset/2799
1204                        from trac.ticket.notification import TicketNotifyEmail
1205                        from trac import config as trac_config
1206                elif version == '0.11':
1207                        from trac import attachment
1208                        from trac.env import Environment
1209                        from trac.ticket import Ticket
1210                        from trac.web.href import Href
1211                        from trac import config as trac_config
1212                        from trac import util
1213                        #
1214                        # return  util.text.to_unicode(str)
1215                        #
1216                        # see http://projects.edgewall.com/trac/changeset/2799
1217                        from trac.ticket.notification import TicketNotifyEmail
1218                else:
1219                        print 'TRAC version %s is not supported' %version
1220                        sys.exit(1)
1221                       
1222                if settings.has_key('enable_syslog'):
1223                        if SYSLOG_AVAILABLE:
1224                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1225
1226                env = Environment(settings['project'], create=0)
1227                tktparser = TicketEmailParser(env, settings, float(version))
1228                tktparser.parse(sys.stdin)
1229
1230        # Catch all errors ans log to SYSLOG if we have enabled this
1231        # else stdout
1232        #
1233        except Exception, error:
1234                if ENABLE_SYSLOG:
1235                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1236
1237                        etype, evalue, etb = sys.exc_info()
1238                        for e in traceback.format_exception(etype, evalue, etb):
1239                                syslog.syslog(e)
1240
1241                        syslog.closelog()
1242                else:
1243                        traceback.print_exc()
1244
1245                if m:
1246                        tktparser.save_email_for_debug(m, True)
1247
1248# EOB
Note: See TracBrowser for help on using the repository browser.