source: trunk/email2trac.py.in @ 228

Last change on this file since 228 was 227, checked in by bas, 15 years ago

email2trac.py.in:

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