source: trunk/email2trac.py.in @ 221

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

email2trac.py.in:

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