source: trunk/email2trac.py.in @ 621

Last change on this file since 621 was 621, checked in by bas, 11 years ago

added support for trac installations in a virtual environment, eg:

  • configure --virtualenv=/data/virtualenvs/trac
  • set/override it with email2trac --virtualenv=/data/virtualenv/trac_1.2

Updated release version to 2.7

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 87.1 KB
Line 
1#!@PYTHON@
2#
3# This file is part of the email2trac utils
4#
5#       Copyright 2002 SARA
6#
7#       Licensed under the Apache License, Version 2.0 (the "License");
8#       you may not use this file except in compliance with the License.
9#       You may obtain a copy of the License at
10#
11#       http://www.apache.org/licenses/LICENSE-2.0
12#
13#       Unless required by applicable law or agreed to in writing, software
14#       distributed under the License is distributed on an "AS IS" BASIS,
15#       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16#       See the License for the specific language governing permissions and
17#       limitations under the License.
18#
19# For vi/emacs or other use tabstop=4 (vi: set ts=4)
20#
21"""
22email2trac.py -- Email -> TRAC tickets
23
24A MTA filter to create Trac tickets from inbound emails.
25
26first proof of concept from:
27 Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28 Copyright 2005, Edgewall Software
29
30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
33
34How to use
35----------
36 * See https://subtrac.sara.nl/oss/email2trac/
37
38 * Commandline opions:
39                -h,--help
40                -d, --debug
41                -e, --virtualenv <path>
42                -f,--file  <configuration file>
43                -n,--dry-run
44                -p, --project <project name>
45                -t, --ticket_prefix <name>
46
47SVN Info:
48        $Id: email2trac.py.in 621 2013-06-17 19:25:11Z bas $
49"""
50import os
51import sys
52import string
53import getopt
54import time
55import email
56import email.Iterators
57import email.Header
58import re
59import urllib
60import unicodedata
61import mimetypes
62import traceback
63import logging
64import logging.handlers
65import UserDict
66import tempfile
67
68from datetime import timedelta, datetime
69from stat import *
70
71## Some global variables
72#
73m = None
74
75# This is to for the function AlwaysNotifyReporter
76sender_email = None
77
78class SaraDict(UserDict.UserDict):
79    def __init__(self, dictin = None):
80        UserDict.UserDict.__init__(self)
81        self.name = None
82       
83        if dictin:
84            if dictin.has_key('name'):
85                self.name = dictin['name']
86                del dictin['name']
87            self.data = dictin
88           
89    def get_value(self, name):
90        if self.has_key(name):
91            return self[name]
92        else:
93            return None
94               
95    def __repr__(self):
96        return repr(self.data)
97
98    def __str__(self):
99        return str(self.data)
100           
101    def __getattr__(self, name):
102        """
103        override the class attribute get method. Return the value
104        from the dictionary
105        """
106        if self.data.has_key(name):
107            return self.data[name]
108        else:
109            return None
110           
111    def __setattr__(self, name, value):
112        """
113        override the class attribute set method only when the UserDict
114        has set its class attribute
115        """
116        if self.__dict__.has_key('data'):
117            self.data[name] = value
118        else:
119            self.__dict__[name] = value
120
121    def __iter__(self):
122        return iter(self.data.keys())
123
124class TicketEmailParser(object):
125    env = None
126    comment = '> '
127
128    def __init__(self, env, parameters, logger, version):
129        self.env = env
130
131        # Database connection
132        #
133        self.db = None
134
135        # Save parameters
136        #
137        self.parameters = parameters
138        self.logger = logger
139
140        # Some useful mail constants
141        #
142        self.email_name = None
143        self.email_addr = None
144        self.email_from = None
145        self.author     = None
146        self.id         = None
147
148        ## Will be set to True if the user has ben registered
149        #
150        self.allow_registered_user = False
151       
152        self.STRIP_CONTENT_TYPES = list()
153
154        ## fields properties via body_text
155        #
156        self.properties = dict()
157
158        self.VERSION = version
159
160        self.get_config = self.env.config.get
161
162        ## init function ##
163        #
164        self.setup_parameters()
165
166    def setup_parameters(self):
167        if self.parameters.umask:
168            os.umask(self.parameters.umask)
169
170        if not self.parameters.spam_level:
171            self.parameters.spam_level = 0
172
173        if not self.parameters.spam_header:
174            self.parameters.spam_header = 'X-Spam-Score'
175
176        if not self.parameters.email_quote:
177            self.parameters.email_quote = '^> .*'
178            self.parameters.skip_line_regex = '^> .*'
179
180        if not self.parameters.ticket_update_by_subject_lookback:
181            self.parameters.ticket_update_by_subject_lookback = 30
182
183        if self.parameters.verbatim_format == None:
184            self.parameters.verbatim_format = 1
185
186        if self.parameters.reflow == None:
187            self.parameters.reflow = 1
188
189        if self.parameters.binhex:
190            self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
191
192        if self.parameters.applesingle:
193            self.STRIP_CONTENT_TYPES.append('application/applefile')
194
195        if self.parameters.appledouble:
196            self.STRIP_CONTENT_TYPES.append('application/applefile')
197
198        if self.parameters.strip_content_types:
199            items = self.parameters.strip_content_types.split(',')
200            for item in items:
201                self.STRIP_CONTENT_TYPES.append(item.strip())
202
203        if self.parameters.tmpdir:
204            self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
205        else:
206            self.parameters.tmpdir = os.path.normcase('/tmp')
207
208        if self.parameters.email_triggers_workflow == None:
209            self.parameters.email_triggers_workflow = 1
210
211        if not self.parameters.subject_field_separator:
212            self.parameters.subject_field_separator = '&'
213        else:
214            self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
215
216        if not self.parameters.strip_signature_regex:
217            self.parameters.strip_signature_regex = '^-- $'
218            self.parameters.cut_line_regex = '^-- $'
219
220        self.trac_smtp_from = self.get_config('notification', 'smtp_from')
221        self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
222
223
224        self.system = None
225
226########## Email Header Functions ###########################################################
227
228    def spam(self, message):
229        """
230        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
231        # Note if Spam_level then '*' are included
232        """
233        self.logger.debug('function spam')
234
235        spam = False
236        if message.has_key(self.parameters.spam_header):
237            spam_l = string.split(message[self.parameters.spam_header])
238            #self.logger.info('Spam header: %s' %(message[self.parameters.spam_header]))
239
240            try:
241                number = spam_l[0].count('*')
242            except IndexError, detail:
243                number = 0
244               
245            if number >= self.parameters.spam_level:
246                spam = True
247               
248        ## treat virus mails as spam
249        #
250        elif message.has_key('X-Virus-found'):         
251            spam = True
252
253        ## Astaro firewall spam handling
254        #
255        elif message.get('X-Spam-Flag') == "YES" and message.get('X-Spam-Result') == "Spam":
256            spam = True
257
258        ## How to handle SPAM messages
259        #
260        if self.parameters.drop_spam and spam:
261
262            self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
263            return 'drop'   
264
265        elif spam:
266
267            return 'Spam'   
268        else:
269
270            return False
271
272    def email_header_acl(self, keyword, message_field, default):
273        """
274        This function wil check if the email address is allowed or denied
275        to send mail to the ticket list
276        """
277        self.logger.debug('function email_header_acl: %s' %keyword)
278
279        try:
280            mail_addresses = self.parameters[keyword]
281
282            # Check if we have an empty string
283            #
284            if not mail_addresses:
285                return default
286
287        except KeyError, detail:
288            self.logger.debug('\t %s not defined, all messages are allowed.' %(keyword))
289
290            return default
291
292        mail_addresses = string.split(mail_addresses, ',')
293        message_addresses = string.split(message_field, ',')
294
295        for entry in mail_addresses:
296            entry = entry.strip()
297            TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
298
299            for addr in message_addresses:
300                addr = addr.strip()
301
302                if self.parameters.compare_function_list in [ 'matches', 'match']:
303                    s = '\t%s matches %s' %(addr, entry)
304                    result =  TO_RE.match(addr)
305                else:
306                    s = '\t%s contains %s' %(addr, entry)
307                    result =  TO_RE.search(addr)
308
309                if result:
310                    self.logger.debug(s)
311                    return True
312
313        return False
314
315    def email_header_txt(self, m):
316        """
317        Display To and CC addresses in description field
318        """
319        s = ''
320
321        if m['To'] and len(m['To']) > 0:
322            s = "'''To:''' %s\r\n" %(m['To'])
323        if m['Cc'] and len(m['Cc']) > 0:
324            s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
325
326        return  self.email_to_unicode(s)
327
328
329    def get_sender_info(self, message):
330        """
331        Get the default author name and email address from the message
332        """
333        self.logger.debug('function get_sender_info')
334
335        to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
336        self.email_to_addrs = list()
337
338        for n,e in to_addrs:
339            self.email_to_addrs.append(e)
340
341        self.email_to_addrs = ','.join(self.email_to_addrs)
342
343
344        self.email_name, self.email_addr  = email.Utils.parseaddr(message['from'])
345
346        ## decode email name can contain charset
347        #
348        self.email_name = self.email_to_unicode(self.email_name)
349
350        dstr = '\t email name: %s, email address: %s' %(self.email_name, self.email_addr) 
351        self.logger.debug(dstr)
352
353        ## Trac can not handle author's name that contains spaces
354        #
355        if self.email_addr.lower() == self.trac_smtp_from.lower():
356            if self.email_name:
357                self.author = self.email_name
358            else:
359                self.author = "email2trac"
360        else:
361            self.author = self.email_addr
362
363        if self.parameters.ignore_trac_user_settings:
364            return
365
366        # Is this a registered user, use email address as search key:
367        # result:
368        #   u : login name
369        #   n : Name that the user has set in the settings tab
370        #   e : email address that the user has set in the settings tab
371        #
372        users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
373            if (
374                (e and (e.lower() == self.email_addr.lower())) or
375                (u.lower() + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
376            )
377            ]
378
379        if len(users) >= 1:
380            self.email_from = users[0][2]
381            self.author = users[0][0]
382
383            self.logger.debug('\tget_sender_info: found registered user: %s' %(self.author) )
384
385            if self.parameters.white_list_registered_users:
386                self.logger.debug('\tget_sender_info: added registered user to white list')
387                self.allow_registered_user = True
388                   
389
390    def set_cc_fields(self, ticket, message, update=False):
391        """
392        Set all the right fields for a new ticket
393        """
394        self.logger.debug('function set_cc_fields')
395
396
397        # Put all CC-addresses in ticket CC field
398        #
399        if self.parameters.reply_all:
400
401            email_cc = ''
402
403            cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
404
405            if not cc_addrs:
406                return
407
408            ## Build a list of forbidden CC addresses
409            #
410            #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
411            #to_list = list()
412            #for n,e in to_addrs:
413            #   to_list.append(e)
414
415            if update:
416                self.logger.debug("\tupdate cc-fields")
417
418            for name,addr in cc_addrs:
419       
420                ## Prevent mail loop
421                #
422                if addr == self.trac_smtp_from:
423                    self.logger.debug("\tSkipping %s email address for CC-field, same as smtp_from address in trac.ini " %(addr))
424                    continue
425
426                ## Alwyas remove reporter email address
427                #
428                elif addr == self.email_addr:
429                    self.logger.debug("\tSkipping reporter email address for CC-field")
430                    continue
431
432                else:
433                    if email_cc:
434                        email_cc = '%s, %s' %(email_cc, addr)
435                    else:
436                        email_cc = addr
437
438            if email_cc:
439                self.logger.debug('\tCC fields: %s' %email_cc)
440                ticket['cc'] = self.email_to_unicode(email_cc)
441
442
443    def acl_list_from_file(self, f, keyword):
444        """
445        Read the email address from a file
446        """
447        self.logger.debug('function acl_list_from_file %s : %s' %(f, keyword))
448       
449        if  not os.path.isfile(f):
450            self.logger.error('%s_file: %s does not exists' %(keyword, f) )
451        else:
452            ## read whole file and replace '\n' with ''
453            #
454            addr_l = open(f, 'r').readlines()
455            s = ','.join(addr_l).replace('\n','')
456
457            try:
458                self.parameters[keyword] = "%s,%s" %(self.parameters[keyword], s)
459            except KeyError, detail:
460                self.parameters[keyword] = s
461
462########## DEBUG functions  ###########################################################
463
464    def debug_body(self, message_body, temp_file):
465        """
466        """
467        self.logger.debug('function debug_body:')
468
469        body_file = "%s.body" %(temp_file)
470        fx = open(body_file, 'wb')
471
472        if self.parameters.dry_run:
473            self.logger.info('DRY-RUN: not saving body to %s' %(body_file))
474            return
475
476        self.logger.debug('writing body to %s' %(body_file))
477        if not message_body:
478                message_body = '(None)'
479
480        message_body = message_body.encode('utf-8')
481
482        fx.write(message_body)
483        fx.close()
484        try:
485            os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
486        except OSError:
487            pass
488
489    def debug_attachments(self, message_parts, temp_file):
490        """
491        """
492        self.logger.debug('function debug_attachments')
493       
494        n = 0
495        for item in message_parts:
496
497            ## Skip inline text parts
498            #
499            if not isinstance(item, tuple):
500                continue
501               
502            (original, filename, part) = item
503            self.logger.debug('\t part%d: Content-Type: %s' % (n, part.get_content_type()) )
504
505            dummy_filename, ext = os.path.splitext(filename)
506
507            n = n + 1
508            part_name = 'part%d%s' %(n, ext)
509            part_file = "%s.%s" %(temp_file, part_name)
510
511            s = 'writing %s: filename: %s' %(part_file, filename)
512            self.print_unicode(s)
513
514            ## Forbidden chars, just write part files instead of names
515            #
516            #filename = filename.replace('\\', '_')
517            #filename = filename.replace('/', '_')
518            #filename = filename + '.att_email2trac'
519            # part_file = os.path.join(self.parameters.tmpdir, filename)
520            #part_file = util.text.unicode_quote(part_file)
521            #self.print_unicode(part_file)
522
523            if self.parameters.dry_run:
524                self.logger.info('DRY_RUN: NOT saving attachments')
525                continue
526
527            fx = open(part_file, 'wb')
528            text = part.get_payload(decode=1)
529
530            if not text:
531                text = '(None)'
532
533            fx.write(text)
534            fx.close()
535
536            try:
537                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
538            except OSError:
539                pass
540
541    def save_email_for_debug(self, message, project_name, create_tempfile=False):
542
543        if self.parameters.dry_run:
544            self.logger.debug('DRY_RUN: NOT saving email message')
545            return
546
547        if create_tempfile:
548            (fd, tmp_file) = tempfile.mkstemp('.%s.email2trac' %project_name)
549            fx = os.fdopen(fd, 'wb')
550        else:
551            tmp_file = os.path.join(self.parameters.tmpdir, 'msg.%s.email2trac' %project_name)
552            fx = open(tmp_file, 'wb')
553
554
555        self.logger.debug('saving email to %s' %(tmp_file))
556        fx.write('%s' % message)
557        fx.close()
558       
559        try:
560            os.chmod(tmp_file, S_IRWXU|S_IRWXG|S_IRWXO)
561        except OSError:
562            pass
563
564        message_parts = self.get_message_parts(message)
565        message_parts = self.unique_attachment_names(message_parts)
566        body_text = self.get_body_text(message_parts)
567        self.debug_body(body_text, tmp_file)
568        self.debug_attachments(message_parts, tmp_file)
569
570########## Conversion functions  ###########################################################
571
572    def email_to_unicode(self, message_str):
573        """
574        Email has 7 bit ASCII code, convert it to unicode with the charset
575        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
576        understands it.
577        """
578        self.logger.debug("function email_to_unicode")
579
580        results =  email.Header.decode_header(message_str)
581
582        s = None
583        for text,format in results:
584            if format:
585                try:
586                    temp = unicode(text, format)
587                except UnicodeError, detail:
588                    # This always works
589                    #
590                    msg = 'ERROR: Could not find charset: %s, please install' %format
591                    self.logger.error(msg)
592                    temp = unicode(text, 'iso-8859-15')
593                except LookupError, detail:
594                    msg = 'ERROR: Could not find charset: %s, please install' %format
595                    self.logger.error(msg)
596                    #temp = unicode(text, 'iso-8859-15')
597                    temp = message_str
598                       
599            else:
600                temp = string.strip(text)
601                temp = unicode(text, 'iso-8859-15')
602
603            if s:
604                s = '%s %s' %(s, temp)
605            else:
606                s = '%s' %temp
607
608        return s
609
610    def str_to_dict(self, s):
611        """
612        Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
613        """
614        self.logger.debug("function str_to_dict")
615
616        fields = string.split(s, self.parameters.subject_field_separator)
617
618        result = dict()
619        for field in fields:
620            try:
621                index, value = string.split(field, '=')
622
623                # We can not change the description of a ticket via the subject
624                # line. The description is the body of the email
625                #
626                if index.lower() in ['description']:
627                    continue
628
629                if value:
630                    result[index.lower()] = value
631
632            except ValueError:
633                pass
634        return result
635
636    def print_unicode(self,s):
637        """
638        This function prints unicode strings if possible else it will quote it
639        """
640        try:
641            self.logger.debug(s)
642        except UnicodeEncodeError, detail:
643            self.logger.debug(util.text.unicode_quote(s))
644
645
646    def html_2_txt(self, data):
647        """
648        Various routines to convert html syntax to valid trac wiki syntax
649        """
650        self.logger.debug('function html_2_txt')
651
652        ## This routine make an safe html that can be include
653        #  in trac, but no further text processing can be done
654        #
655#       try:
656#           from lxml.html.clean import Cleaner
657#           tags_rm = list()
658#           tags_rm.append('body')
659#
660#           cleaner = Cleaner(remove_tags=tags_rm )
661#           parsed_data = cleaner.clean_html(data)
662#           parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n'
663#
664#           return parsed_data
665#           
666#       except ImportError::
667#           pass
668
669        parsed_data = None
670        if self.parameters.html2text_cmd:
671            (fd, tmp_file) = tempfile.mkstemp('email2trac.html')
672            f = os.fdopen(fd, 'w')
673
674            cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file)
675            self.logger.debug('\t html2text conversion %s'%(cmd))
676   
677            if self.parameters.dry_run:
678                self.logger.info('DRY_RUN: html2text conversion command: %s\n' %(cmd))
679
680            else:
681                f.write(data)
682                f.close()
683
684                lines = os.popen(cmd).readlines()
685                parsed_data =  ''.join(lines)
686
687                os.unlink(tmp_file)
688
689        else:
690            self.logger.debug('\t No html2text conversion tool specified in email2trac.conf')
691
692        return parsed_data
693
694    def check_filename_length(self, name):
695        """
696        To bypass a bug in Trac
697        check if the filename lenght is not larger then OS limit.
698          yes : return truncated filename
699          no  : return unmodified filename
700        """
701        self.logger.debug('function check_filename_length: ')
702
703        if not name:
704            return 'None'
705       
706        dummy_filename, ext = os.path.splitext(name)
707
708        ## Trac uses this format
709        #
710        try:
711            quote_format = util.text.unicode_quote(dummy_filename)
712
713        except UnicodeDecodeError, detail:
714            ## last resort convert to unicode
715            #
716            dummy_filename = util.text.to_unicode(dummy_filename)
717            quote_format = util.text.unicode_quote(dummy_filename)
718
719        ## Determine max filename lenght
720        #
721        try:
722            filemax_length = os.pathconf('/', 'PC_NAME_MAX')
723        except AttributeError, detail:
724            filemax_lenght = 240
725
726        if len(quote_format) <= filemax_length:
727            return name
728
729        else:
730            ## Truncate file to filemax_length and reserve room for extension
731            #  We must break on a boundry
732            #
733            length = filemax_length - 6
734
735            for i in range(0,10):
736
737                truncated = quote_format[ : (length - i)]
738
739                try:
740                    unqoute_truncated = util.text.unicode_unquote(truncated)
741                    unqoute_truncated = unqoute_truncated + ext
742
743                    self.print_unicode('\t ' + unqoute_truncated)
744
745                    break
746
747                except UnicodeDecodeError, detail:
748                    continue
749
750
751            return unqoute_truncated
752
753########## TRAC ticket functions  ###########################################################
754
755    def mail_workflow(self, tkt):
756        """
757        """
758        self.logger.debug('function mail_workflow: ')
759       
760        req = Mock(authname=self.author, perm=MockPerm(), args={})
761        ticket_system = TicketSystem(self.env)
762       
763        try:
764            workflow = self.parameters['workflow_%s' %tkt['status'].lower()]
765
766        except KeyError:
767            ## fallback for compability (Will be deprecated)
768            #  workflow can be none.
769            #
770            workflow = None
771            if tkt['status'] in ['closed']:
772                workflow = self.parameters.workflow
773
774        if workflow:
775
776            ## process all workflow implementations
777            #
778            tkt_module = TicketModule(self.env)
779            field_changes, problems = tkt_module.get_ticket_changes(req, tkt, workflow)
780
781            for field in field_changes.keys():
782
783                ## We have already processed these fields
784                #
785                if not field in ['summary', 'description']:
786                    s = 'workflow : %s, field %s : %s, by %s' \
787                       %(workflow, field, field_changes[field]['new'],field_changes[field]['by'] )
788                    self.logger.debug(s)
789
790                    tkt[field] = field_changes[field]['new']
791
792            return True
793
794        else:
795            return False
796
797    def check_permission_participants(self, tkt, action):
798        """
799        Check if the mailer is allowed to update the ticket
800        """
801        self.logger.debug('function check_permission_participants %s')
802
803        if tkt['reporter'].lower() in [self.author.lower(), self.email_addr.lower()]:
804            self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
805
806            return True
807
808        perm = PermissionSystem(self.env)
809        if perm.check_permission(action, self.author):
810            self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
811
812            return True
813       
814        # Is the updater in the CC?
815        try:
816            cc_list = tkt['cc'].split(',')
817            for cc in cc_list:
818                if self.email_addr.lower() in cc.lower.strip():
819                    self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
820
821                    return True
822
823        except KeyError:
824            pass
825
826        return False
827
828    def check_permission(self, tkt, action):
829        """
830        check if the reporter has the right permission for the action:
831          - TICKET_CREATE
832          - TICKET_MODIFY
833          - TICKET_APPEND
834          - TICKET_CHGPROP
835
836        There are three models:
837            - None      : no checking at all
838            - trac      : check the permission via trac permission model
839            - email2trac: ....
840        """
841        self.logger.debug("function check_permission: %s" %(action))
842
843        if self.parameters.ticket_permission_system in ['trac']:
844
845            perm = PermissionCache(self.env, self.author)
846            if perm.has_permission(action):
847                return True
848            else:
849                return False
850
851        elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
852            return (self.check_permission_participants(tkt, action))   
853
854        ## Default is to allow everybody ticket updates and ticket creation
855        #
856        else:
857                return True
858
859
860    def update_ticket_fields(self, ticket, user_dict, new=None):
861        """
862        This will update the ticket fields. It will check if the
863        given fields are known and if the right values are specified
864        It will only update the ticket field value:
865            - If the field is known
866            - If the value supplied is valid for the ticket field.
867              If not then there are two options:
868               1) Skip the value (new=None)
869               2) Set default value for field (new=1)
870        """
871        self.logger.debug("function update_ticket_fields")
872
873        ## Check only permission model on ticket updates
874        #
875        if not new:
876            if self.parameters.ticket_permission_system:
877                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
878                    self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
879                    return False
880
881        ## Build a system dictionary from the ticket fields
882        #  with field as index and option as value
883        #
884        sys_dict = dict()
885
886        for field in ticket.fields:
887
888            try:
889                sys_dict[field['name']] = field['options']
890
891            except KeyError:
892                sys_dict[field['name']] = None
893                pass
894
895        ## Check user supplied fields an compare them with the
896        #  system one's
897        #
898        for field,value in user_dict.items():
899       
900            s = 'user_field\t %s = %s' %(field,value)
901            self.print_unicode(s)
902
903            if not field in sys_dict.keys(): 
904                self.logger.debug('%s is not a valid field for tickets' %(field))
905                continue
906
907            ## To prevent mail loop
908            #
909            if field == 'cc':
910
911                cc_list = user_dict['cc'].split(',')
912
913                if self.trac_smtp_from in cc_list:
914                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
915
916                    cc_list.remove(self.trac_smtp_from)
917
918                value = ','.join(cc_list)
919               
920
921            ## Check if every value is allowed for this field
922            #
923            if sys_dict[field]:
924
925                if value in sys_dict[field]:
926                    ticket[field] = value
927                else:
928                    ## Must we set a default if value is not allowed
929                    #
930                    if new:
931                        value = self.get_config('ticket', 'default_%s' %(field) )
932
933            else:
934
935                ticket[field] = value
936
937            s = 'ticket_field\t %s = %s' %(field,  ticket[field])
938            self.print_unicode(s)
939
940    def ticket_update(self, m, id, spam):
941        """
942        If the current email is a reply to an existing ticket, this function
943        will append the contents of this email to that ticket, instead of
944        creating a new one.
945        """
946        self.logger.debug("function ticket_update")
947
948        if not self.parameters.ticket_update:
949            self.logger.debug("ticket_update disabled")
950            return False
951
952        ## Must we update ticket fields
953        #
954        update_fields = dict()
955        try:
956            id, keywords = string.split(id, '?')
957
958            update_fields = self.str_to_dict(keywords)
959
960            ## Strip '#'
961            #
962            self.id = int(id[1:])
963
964        except ValueError:
965
966            ## Strip '#'
967            #
968            self.id = int(id[1:])
969
970        self.logger.debug("\tticket id: %s" %id)
971
972        ## When is the change committed
973        #
974        if self.VERSION < 0.11:
975            when = int(time.time())
976        else:
977            when = datetime.now(util.datefmt.utc)
978
979        try:
980            tkt = Ticket(self.env, self.id, self.db)
981
982        except util.TracError, detail:
983
984            ## Not a valid ticket
985            #
986            self.logger.info("\tCreating a new ticket, ticket id: %s does not exists" %id)
987            self.id = None
988            return False
989
990        ## Check the permission of the reporter
991        #
992        if self.parameters.ticket_permission_system:
993            if not self.check_permission(tkt, 'TICKET_APPEND'):
994                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
995                return False
996
997        ## How many changes has this ticket
998        #
999        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
1000        cnum = sum(1 for e in grouped) + 1
1001
1002
1003        ## reopen the ticket if it is was closed
1004        #  We must use the ticket workflow framework
1005        #
1006        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
1007
1008            if not self.mail_workflow(tkt):
1009
1010                if tkt['status'] in ['closed']:
1011                    tkt['status'] = 'reopened'
1012                    tkt['resolution'] = ''
1013
1014        ## Old pre 0.11 situation
1015        #
1016        elif self.parameters.email_triggers_workflow:
1017
1018            self.logger.debug('\temail triggers workflow pre trac 0.11')
1019
1020            if tkt['status'] in ['closed']:
1021                tkt['status'] = 'reopened'
1022                tkt['resolution'] = ''
1023
1024        else:
1025            self.logger.debug('\temail triggers workflow disabled')
1026
1027        ## Must we update some ticket fields properties via subject line
1028        #
1029        if update_fields:
1030            self.update_ticket_fields(tkt, update_fields)
1031
1032
1033        message_parts = self.get_message_parts(m)
1034        message_parts = self.unique_attachment_names(message_parts)
1035
1036        ## Must we update some ticket fields properties via inline comments
1037        # in body_text
1038        #
1039        if self.properties:
1040                self.update_ticket_fields(tkt, self.properties)
1041
1042        ## Must we update the CC ticket field
1043        #
1044        self.set_cc_fields(tkt, m, update=True)
1045
1046        if self.parameters.email_header:
1047            message_parts.insert(0, self.email_header_txt(m))
1048
1049        body_text = self.get_body_text(message_parts)
1050
1051        error_with_attachments = self.attach_attachments(message_parts)
1052
1053        if body_text.strip() or update_fields or self.properties:
1054
1055            if self.parameters.dry_run:
1056                s = 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) %s %s' %(self.author, cnum)
1057                self.logger.info(s)
1058
1059            else:
1060                if error_with_attachments:
1061                    body_text = '%s\\%s' %(error_with_attachments, body_text)
1062
1063                self.logger.debug('\ttkt.save_changes(%s, %d)' %(self.author, cnum))
1064                tkt.save_changes(self.author, body_text, when, None, str(cnum))
1065           
1066        if not spam:
1067            self.notify(tkt, False, when)
1068
1069        return True
1070
1071    def set_ticket_fields(self, ticket):
1072        """
1073        set the ticket fields to value specified
1074            - /etc/email2trac.conf with <prefix>_<field>
1075            - trac default values, trac.ini
1076        """
1077        self.logger.debug('function set_ticket_fields')
1078
1079        user_dict = dict()
1080
1081        for field in ticket.fields:
1082
1083            name = field['name']
1084
1085            ## default trac value
1086            #
1087            if not field.get('custom'):
1088                value = self.get_config('ticket', 'default_%s' %(name) )
1089
1090                ## skip this field can only be set by email2trac.conf
1091                #
1092                if name in ['resolution']:
1093                    value = None
1094
1095            else:
1096                ##  Else get the default value for reporter
1097                #
1098                value = field.get('value')
1099                options = field.get('options')
1100
1101                if value and options and (value not in options):
1102                     value = options[int(value)]
1103   
1104            s = 'trac[%s] = %s' %(name, value)
1105            self.print_unicode(s)
1106
1107            ## email2trac.conf settings
1108            #
1109            prefix = self.parameters.ticket_prefix
1110            try:
1111                value = self.parameters['%s_%s' %(prefix, name)]
1112
1113                s = 'email2trac[%s] = %s ' %(name, value)
1114                self.print_unicode(s)
1115
1116            except KeyError, detail:
1117                pass
1118       
1119            if value:
1120                user_dict[name] = value
1121
1122                s = 'used %s = %s' %(name, value)
1123                self.print_unicode(s)
1124
1125        self.update_ticket_fields(ticket, user_dict, new=1)
1126
1127        if 'status' not in user_dict.keys():
1128            ticket['status'] = 'new'
1129
1130
1131    def ticket_update_by_subject(self, subject):
1132        """
1133        This list of Re: prefixes is probably incomplete. Taken from
1134        wikipedia. Here is how the subject is matched
1135          - Re: <subject>
1136          - Re: (<Mail list label>:)+ <subject>
1137
1138        So we must have the last column
1139        """
1140        self.logger.debug('function ticket_update_by_subject')
1141
1142        found_id = None
1143        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
1144               
1145            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
1146            result = SUBJECT_RE.search(subject)
1147
1148            if result:
1149                ## This is a reply
1150                #
1151                orig_subject = result.group(1)
1152
1153                self.logger.debug('subject search string: %s' %(orig_subject))
1154
1155                cursor = self.db.cursor()
1156                summaries = [orig_subject, '%%: %s' % orig_subject]
1157
1158                ## Time resolution is in micoseconds
1159                #
1160                search_date = datetime.now(util.datefmt.utc) - timedelta(days=self.parameters.ticket_update_by_subject_lookback)
1161
1162                if self.VERSION < 0.12:
1163                    lookback = util.datefmt.to_timestamp(search_date)
1164                else:
1165                    lookback = util.datefmt.to_utimestamp(search_date)
1166
1167                for summary in summaries:
1168                    self.logger.debug('Looking for summary matching: "%s"' % summary)
1169
1170                    sql = """SELECT id, reporter FROM ticket
1171                            WHERE changetime >= %s AND summary LIKE %s
1172                            ORDER BY changetime DESC"""
1173
1174                    cursor.execute(sql, [lookback, summary.strip()])
1175
1176                    for row in cursor:
1177
1178                        (matched_id, sql_reporter) = row
1179
1180                        ## Save first entry.
1181                        #
1182                        if not found_id:
1183                            found_id = matched_id
1184                           
1185                        ## If subject and reporter are the same. The we certainly have found the right ticket
1186                        #
1187                        if sql_reporter == self.author:
1188                            self.logger.debug('Found matching reporter: %s with ticket id: %d' %(sql_reporter, matched_id))
1189                            found_id = matched_id
1190                            break
1191
1192                    if found_id:
1193                        self.logger.debug('Found matching ticket id: %d' % found_id)
1194                        found_id = '#%d' % found_id
1195                        return (found_id, orig_subject)
1196                   
1197                    # obsolete !?? 12 Aug 2011
1198                    #subject = orig_subject
1199
1200        return (found_id, subject)
1201
1202
1203    def new_ticket(self, msg, subject, spam, set_fields = None):
1204        """
1205        Create a new ticket
1206        """
1207        self.logger.debug('function new_ticket')
1208
1209        tkt = Ticket(self.env)
1210
1211        ## self.author can be email address of an username
1212        #
1213        tkt['reporter'] = self.author
1214
1215        self.set_cc_fields(tkt, msg)
1216
1217        self.set_ticket_fields(tkt)
1218
1219        ## Check the permission of the reporter
1220        #
1221        if self.parameters.ticket_permission_system:
1222            if not self.check_permission(tkt, 'TICKET_CREATE'):
1223                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1224                return False
1225
1226        ## Old style setting for component, will be removed
1227        #
1228        if spam:
1229            tkt['component'] = 'Spam'
1230
1231        elif self.parameters.has_key('component'):
1232            tkt['component'] = self.parameters['component']
1233
1234        if not msg['Subject']:
1235            tkt['summary'] = u'(No subject)'
1236        else:
1237            tkt['summary'] = subject
1238
1239
1240        if set_fields:
1241            rest, keywords = string.split(set_fields, '?')
1242
1243            if keywords:
1244                update_fields = self.str_to_dict(keywords)
1245                self.update_ticket_fields(tkt, update_fields)
1246
1247
1248        message_parts = self.get_message_parts(msg, True)
1249
1250        ## Must we update some ticket fields properties via body_text
1251        #
1252        if self.properties:
1253                self.update_ticket_fields(tkt, self.properties)
1254
1255        message_parts = self.unique_attachment_names(message_parts)
1256       
1257        ## produce e-mail like header
1258        #
1259        head = ''
1260        if self.parameters.email_header:
1261            head = self.email_header_txt(msg)
1262            message_parts.insert(0, head)
1263           
1264        body_text = self.get_body_text(message_parts)
1265
1266        tkt['description'] = body_text
1267
1268        ## When is the change committed
1269        #
1270        if self.VERSION < 0.11:
1271            when = int(time.time())
1272        else:
1273            when = datetime.now(util.datefmt.utc)
1274
1275        if self.parameters.dry_run:
1276            self.logger.info('DRY_RUN: tkt.insert()')
1277        else:
1278            self.id = tkt.insert()
1279   
1280        changed = False
1281        comment = ''
1282
1283        ## some routines in trac are dependend on ticket id
1284        #  like alternate notify template
1285        #
1286        if self.parameters.alternate_notify_template:
1287            tkt['id'] = self.id
1288            changed = True
1289
1290        ## Rewrite the description if we have mailto enabled
1291        #
1292        if self.parameters.mailto_link:
1293            changed = True
1294            comment = u'\nadded mailto line\n'
1295
1296            #mailto = self.html_mailto_link( m['Subject'])
1297            mailto = self.html_mailto_link(subject)
1298
1299            tkt['description'] = u'%s\r\n%s%s\r\n' \
1300                %(head, mailto, body_text)
1301   
1302        ## Save the attachments to the ticket   
1303        #
1304        error_with_attachments =  self.attach_attachments(message_parts)
1305
1306        if error_with_attachments:
1307            changed = True
1308            comment = '%s\n%s\n' %(comment, error_with_attachments)
1309
1310        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
1311            if self.mail_workflow(tkt):
1312                changed = True
1313
1314        if changed:
1315            if self.parameters.dry_run:
1316                s = 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1317                self.logger.info(s)
1318
1319            else:
1320                tkt.save_changes(tkt['reporter'], comment)
1321
1322        if not spam:
1323            self.notify(tkt, True)
1324
1325
1326    def attach_attachments(self, message_parts, update=False):
1327        '''
1328        save any attachments as files in the ticket's directory
1329        '''
1330        self.logger.debug('function attach_attachments()')
1331
1332        if self.parameters.dry_run:
1333            self.logger.debug("DRY_RUN: no attachments attached to tickets")
1334            return ''
1335
1336        count = 0
1337
1338        ## Get Maxium attachment size
1339        #
1340        max_size = int(self.get_config('attachment', 'max_size'))
1341        status   = None
1342       
1343        for item in message_parts:
1344            ## Skip body parts
1345            #
1346            if not isinstance(item, tuple):
1347                continue
1348               
1349            (original, filename, part) = item
1350
1351            ## We have to determine the size so we use this temporary solution.
1352            #
1353            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, 'email2trac_tmp.att'))
1354            text = part.get_payload(decode=1)
1355            if not text:
1356                text = '(None)'
1357            fd.write(text)
1358            fd.close()
1359
1360            ## get the file_size
1361            #
1362            stats = os.lstat(path)
1363            file_size = stats[ST_SIZE]
1364
1365            ## Check if the attachment size is allowed
1366            #
1367            if (max_size != -1) and (file_size > max_size):
1368                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1369                    %(status, original, file_size, max_size)
1370
1371                os.unlink(path)
1372                continue
1373            else:
1374                count = count + 1
1375                   
1376            ## Insert the attachment
1377            #
1378            fd = open(path, 'rb')
1379            if self.system == 'discussion':
1380                att = attachment.Attachment(self.env, 'discussion', 'topic/%s' % (self.id,))
1381            elif self.system == 'blog':
1382                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,))
1383            else:
1384                s = 'Attach %s to ticket %d' %(filename, self.id)
1385                self.print_unicode(s)
1386                att = attachment.Attachment(self.env, 'ticket', self.id)
1387 
1388            ## This will break the ticket_update system, the body_text is vaporized
1389            #  ;-(
1390            #
1391            if not update:
1392                att.author = self.author
1393                att.description = self.email_to_unicode('Added by email2trac')
1394
1395            try:
1396
1397                self.logger.debug('Insert atachment')
1398                att.insert(filename, fd, file_size)
1399
1400            except OSError, detail:
1401
1402                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1403                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
1404
1405            ## Remove the created temporary filename
1406            #
1407            fd.close()
1408            os.unlink(path)
1409
1410        ## return error
1411        #
1412        return status
1413
1414########## Fullblog functions  #################################################
1415
1416    def blog(self, msg, subject, id, params):
1417        """
1418        The blog create/update function
1419        """
1420        ## import the modules
1421        #
1422        from tracfullblog.core import FullBlogCore
1423        from tracfullblog.model import BlogPost, BlogComment
1424
1425        ## instantiate blog core
1426        #
1427        blog = FullBlogCore(self.env)
1428        req = Mock(authname='anonymous', perm=MockPerm(), args={})
1429
1430        ## parameters from the subject
1431        #
1432        params = self.str_to_dict((params or '').lstrip('?'))
1433
1434        ## preferably get the time from the email date header, or else
1435        #  use current date and time
1436        date = email.Utils.parsedate_tz(msg.get('date'))
1437        if date:
1438            dt = util.datefmt.to_datetime(email.Utils.mktime_tz(date), util.datefmt.utc)
1439        else:
1440            self.logger.warn("No valid date header found")
1441            dt = util.datefmt.to_datetime(None, util.datefmt.utc)
1442
1443        ## blog entry affected
1444        #
1445        self.id = id or util.datefmt.format_datetime(dt, "%Y%m%d%H%M%S", util.datefmt.utc)
1446
1447        ## check wether a blog post exists
1448        #
1449        post = BlogPost(self.env, self.id)
1450        force_update = self.properties.get('update', params.get('update'))
1451
1452        ## message parts
1453        #
1454        message_parts = self.get_message_parts(msg)
1455        message_parts = self.unique_attachment_names(message_parts)
1456
1457        if post.get_versions() and not force_update:
1458
1459            ## add comment to blog entry
1460            #
1461            comment = BlogComment(self.env, self.id)
1462            comment.author = self.properties.get('author', params.get('author', self.author))
1463            comment.comment = self.get_body_text(message_parts)
1464            comment.time = dt
1465
1466            if self.parameters.dry_run:
1467                self.logger.info('DRY-RUN: not adding comment for blog entry "%s"' % id)
1468                return
1469            warnings = blog.create_comment(req, comment)
1470
1471        else:
1472            ## create or update blog entry
1473            #
1474            post.author = self.properties.get('author', params.get('author', self.author))
1475            post.categories = self.properties.get('categories', params.get('categories', ''))
1476            post.title = subject.strip()
1477            post.publish_time = dt
1478            post.body = self.get_body_text(message_parts)
1479           
1480            if self.parameters.dry_run:
1481                self.logger.info('DRY-RUN: not creating blog entry "%s"' % post.title)
1482                return
1483            warnings = blog.create_post(req, post, self.author, u'Created by email2trac', False)
1484
1485        ## check for problems
1486        #
1487        if warnings:
1488            raise TracError(', '.join('blog:%s:%s' % (w[0], w[1]) for w in warnings))
1489       
1490        ## all seems well, attach attachments
1491        #
1492        self.attach_attachments(message_parts)
1493
1494
1495########## Discussion functions  ##############################################
1496
1497    def discussion_topic(self, content, subject):
1498
1499        ## Import modules.
1500        #
1501        from tracdiscussion.api import DiscussionApi
1502        from trac.util.datefmt import to_timestamp, utc
1503
1504        self.logger.debug('Creating a new topic in forum:', self.id)
1505
1506        ## Get dissussion API component.
1507        #
1508        api = self.env[DiscussionApi]
1509        context = self._create_context(content, subject)
1510
1511        ## Get forum for new topic.
1512        #
1513        forum = api.get_forum(context, self.id)
1514
1515        if not forum:
1516            self.logger.error("ERROR: Replied forum doesn't exist")
1517
1518        ## Prepare topic.
1519        #
1520        topic = {'forum' : forum['id'],
1521                 'subject' : context.subject,
1522                 'time': to_timestamp(datetime.now(utc)),
1523                 'author' : self.author,
1524                 'subscribers' : [self.email_addr],
1525                 'body' : self.get_body_text(context.content_parts)}
1526
1527        ## Add topic to DB and commit it.
1528        #
1529        self._add_topic(api, context, topic)
1530        self.db.commit()
1531
1532    def discussion_topic_reply(self, content, subject):
1533
1534        ## Import modules.
1535        #
1536        from tracdiscussion.api import DiscussionApi
1537        from trac.util.datefmt import to_timestamp, utc
1538
1539        self.logger.debug('Replying to discussion topic', self.id)
1540
1541        ## Get dissussion API component.
1542        #
1543        api = self.env[DiscussionApi]
1544        context = self._create_context(content, subject)
1545
1546        ## Get replied topic.
1547        #
1548        topic = api.get_topic(context, self.id)
1549
1550        if not topic:
1551            self.logger.error("ERROR: Replied topic doesn't exist")
1552
1553        ## Prepare message.
1554        #
1555        message = {'forum' : topic['forum'],
1556                   'topic' : topic['id'],
1557                   'replyto' : -1,
1558                   'time' : to_timestamp(datetime.now(utc)),
1559                   'author' : self.author,
1560                   'body' : self.get_body_text(context.content_parts)}
1561
1562        ## Add message to DB and commit it.
1563        #
1564        self._add_message(api, context, message)
1565        self.db.commit()
1566
1567    def discussion_message_reply(self, content, subject):
1568
1569        ## Import modules.
1570        #
1571        from tracdiscussion.api import DiscussionApi
1572        from trac.util.datefmt import to_timestamp, utc
1573
1574        self.logger.debug('Replying to discussion message', self.id)
1575
1576        ## Get dissussion API component.
1577        #
1578        api = self.env[DiscussionApi]
1579        context = self._create_context(content, subject)
1580
1581        ## Get replied message.
1582        #
1583        message = api.get_message(context, self.id)
1584
1585        if not message:
1586            self.logger.error("ERROR: Replied message doesn't exist")
1587
1588        ## Prepare message.
1589        #
1590        message = {'forum' : message['forum'],
1591                   'topic' : message['topic'],
1592                   'replyto' : message['id'],
1593                   'time' : to_timestamp(datetime.now(utc)),
1594                   'author' : self.author,
1595                   'body' : self.get_body_text(context.content_parts)}
1596
1597        ## Add message to DB and commit it.
1598        #
1599        self._add_message(api, context, message)
1600        self.db.commit()
1601
1602    def _create_context(self, content, subject):
1603
1604        ## Import modules.
1605        #
1606        from trac.mimeview import Context
1607        from trac.web.api import Request
1608        from trac.perm import PermissionCache
1609
1610        ## TODO: Read server base URL from config.
1611        #  Create request object to mockup context creation.
1612        #
1613        environ = {'SERVER_PORT' : 80,
1614                   'SERVER_NAME' : 'test',
1615                   'REQUEST_METHOD' : 'POST',
1616                   'wsgi.url_scheme' : 'http',
1617                   'wsgi.input' : sys.stdin}
1618        chrome =  {'links': {},
1619                   'scripts': [],
1620                   'ctxtnav': [],
1621                   'warnings': [],
1622                   'notices': []}
1623
1624        if self.env.base_url_for_redirect:
1625            environ['trac.base_url'] = self.env.base_url
1626
1627        req = Request(environ, None)
1628        req.chrome = chrome
1629        req.tz = 'missing'
1630        req.authname = self.author
1631        req.perm = PermissionCache(self.env, self.author)
1632        req.locale = None
1633
1634        ## Create and return context.
1635        #
1636        context = Context.from_request(req)
1637        context.realm = 'discussion-email2trac'
1638        context.cursor = self.db.cursor()
1639        context.content = content
1640        context.subject = subject
1641
1642        ## Read content parts from content.
1643        #
1644        context.content_parts = self.get_message_parts(content)
1645        context.content_parts = self.unique_attachment_names(
1646          context.content_parts)
1647
1648        return context
1649
1650    def _add_topic(self, api, context, topic):
1651        context.req.perm.assert_permission('DISCUSSION_APPEND')
1652
1653        ## Filter topic.
1654        #
1655        for discussion_filter in api.discussion_filters:
1656            accept, topic_or_error = discussion_filter.filter_topic(
1657              context, topic)
1658            if accept:
1659                topic = topic_or_error
1660            else:
1661                raise TracError(topic_or_error)
1662
1663        ## Add a new topic.
1664        #
1665        api.add_topic(context, topic)
1666
1667        ## Get inserted topic with new ID.
1668        #
1669        topic = api.get_topic_by_time(context, topic['time'])
1670
1671        ## Attach attachments.
1672        #
1673        self.id = topic['id']
1674        self.attach_attachments(context.content_parts, True)
1675
1676        ## Notify change listeners.
1677        #
1678        for listener in api.topic_change_listeners:
1679            listener.topic_created(context, topic)
1680
1681    def _add_message(self, api, context, message):
1682        context.req.perm.assert_permission('DISCUSSION_APPEND')
1683
1684        ## Filter message.
1685        #
1686        for discussion_filter in api.discussion_filters:
1687            accept, message_or_error = discussion_filter.filter_message(
1688              context, message)
1689            if accept:
1690                message = message_or_error
1691            else:
1692                raise TracError(message_or_error)
1693
1694        ## Add message.
1695        #
1696        api.add_message(context, message)
1697
1698        ## Get inserted message with new ID.
1699        #
1700        message = api.get_message_by_time(context, message['time'])
1701
1702        ## Attach attachments.
1703        #
1704
1705        self.attach_attachments(context.content_parts, True)
1706
1707        ## Notify change listeners.
1708        #
1709        for listener in api.message_change_listeners:
1710            listener.message_created(context, message)
1711
1712########## MAIN function  ######################################################
1713
1714    def parse(self, fp):
1715        """
1716        """
1717        self.logger.debug('Main function parse')
1718        global m
1719
1720        m = email.message_from_file(fp)
1721       
1722        if not m:
1723            self.logger.debug('This is not a valid email message format')
1724            return
1725           
1726        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1727        #
1728        try:
1729            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1730        except AttributeError, detail:
1731            pass
1732
1733        if self.parameters.debug:     # save the entire e-mail message text
1734            self.save_email_for_debug(m, self.parameters.project_name, True)
1735
1736        if self.VERSION <= 0.11:
1737            self.db = self.env.get_db_cnx()
1738        else:
1739            self.db = self.env.get_read_db()
1740
1741        self.get_sender_info(m)
1742
1743
1744        if self.parameters.white_list_file:
1745            self.acl_list_from_file(self.parameters.white_list_file, 'white_list')
1746
1747        if not ( self.email_header_acl('white_list', self.email_addr, True) or self.allow_registered_user ) :
1748
1749            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1750            return False
1751
1752        if self.email_header_acl('black_list', self.email_addr, False):
1753            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1754            return False
1755
1756        if not self.email_header_acl('recipient_list', self.email_to_addrs, True):
1757            self.logger.info('Message rejected : %s not in recipient list' %(self.email_to_addrs))
1758            return False
1759
1760        ## If spam drop the message
1761        #
1762        if self.spam(m) == 'drop':
1763            return False
1764
1765        elif self.spam(m) == 'spam':
1766            spam_msg = True
1767        else:
1768            spam_msg = False
1769
1770        if not m['Subject']:
1771            subject  = 'No Subject'
1772        else:
1773            subject  = self.email_to_unicode(m['Subject'])
1774
1775        ## Bug in python logging module <2.6
1776        #
1777        self.logger.info('subject: %s' %repr(subject))
1778
1779        ## Ticket id is in Delivered-To Field. Trac notify must send this
1780        #
1781        #try:
1782        #    print self.parameters.project_name
1783        #    print 'Delivered To', m['Delivered-To']
1784        #    print self.parameters.to_parse_address_delimiter
1785        #    id = m['Delivered-To'].split(self.parameters.to_parse_address_delimiter)[1]
1786        #    print id
1787        #    id = id.split('@')[0]
1788        #    print id
1789#
1790#        except KeyError, detail:
1791#            pass
1792
1793
1794        ## [hic] #1529: Re: LRZ
1795        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1796        #
1797        ticket_regex = r'''
1798            (?P<new_fields>[#][?].*)
1799            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1800            '''
1801        ## Check if  FullBlogPlugin is installed
1802        #
1803        blog_enabled = None
1804        blog_regex = ''
1805        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1806            self.logger.debug('Trac BLOG support enabled')
1807            blog_enabled = True
1808            blog_regex = '''|(?P<blog>blog(?P<blog_params>[?][^:]*)?:(?P<blog_id>\S*))'''
1809
1810
1811        ## Check if DiscussionPlugin is installed
1812        #
1813        discussion_enabled = None
1814        discussion_regex = ''
1815        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1816            self.logger.debug('Trac Discussion support enabled')
1817            discussion_enabled = True
1818            discussion_regex = r'''
1819            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1820            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1821            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1822            '''
1823
1824
1825        regex_str = ticket_regex + blog_regex + discussion_regex
1826        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1827
1828        ## Find out if this is a ticket, a blog or a discussion
1829        #
1830        result =  SYSTEM_RE.search(subject)
1831
1832        if result:
1833            ## update ticket + fields
1834            #
1835            if result.group('reply'):
1836                self.system = 'ticket'
1837
1838                ## Skip the last ':' character
1839                #
1840                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1841                    self.new_ticket(m, subject, spam_msg)
1842
1843            ## New ticket + fields
1844            #
1845            elif result.group('new_fields'):
1846                self.system = 'ticket'
1847                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1848
1849            if blog_enabled:
1850                if result.group('blog'):
1851                    self.system = 'blog'
1852                    self.blog(m, subject[result.end('blog'):], result.group('blog_id'), result.group('blog_params'))
1853
1854            if discussion_enabled:
1855                ## New topic.
1856                #
1857                if result.group('forum'):
1858                    self.system = 'discussion'
1859                    self.id = int(result.group('forum_id'))
1860                    self.discussion_topic(m, subject[result.end('forum'):])
1861
1862                ## Reply to topic.
1863                #
1864                elif result.group('topic'):
1865                    self.system = 'discussion'
1866                    self.id = int(result.group('topic_id'))
1867                    self.discussion_topic_reply(m, subject[result.end('topic'):])
1868
1869                ## Reply to topic message.
1870                #
1871                elif result.group('message'):
1872                    self.system = 'discussion'
1873                    self.id = int(result.group('message_id'))
1874                    self.discussion_message_reply(m, subject[result.end('message'):])
1875
1876        else:
1877
1878            self.system = 'ticket'
1879            (matched_id, subject) = self.ticket_update_by_subject(subject)
1880
1881            if matched_id:
1882
1883                if not self.ticket_update(m, matched_id, spam_msg):
1884                    self.new_ticket(m, subject, spam_msg)
1885
1886            else:
1887                ## No update by subject, so just create a new ticket
1888                #
1889                self.new_ticket(m, subject, spam_msg)
1890
1891
1892########## BODY TEXT functions  ###########################################################
1893
1894    def strip_signature(self, text):
1895        """
1896        Strip signature from message, inspired by Mailman software
1897        """
1898        self.logger.debug('function strip_signature: %s' %self.parameters.strip_signature_regex)
1899
1900        body = []
1901
1902        STRIP_RE = re.compile( self.parameters.strip_signature_regex )
1903        for line in text.splitlines():
1904
1905            match = STRIP_RE.match(line)
1906            if match:
1907                self.logger.debug('\t"%s "  matched, skiping rest of message' %line)
1908                break
1909
1910            body.append(line)
1911
1912        return ('\n'.join(body))
1913
1914    def reflow(self, text, delsp = 0):
1915        """
1916        Reflow the message based on the format="flowed" specification (RFC 3676)
1917        """
1918        flowedlines = []
1919        quotelevel = 0
1920        prevflowed = 0
1921
1922        for line in text.splitlines():
1923            from re import match
1924           
1925            ## Figure out the quote level and the content of the current line
1926            #
1927            m = match('(>*)( ?)(.*)', line)
1928            linequotelevel = len(m.group(1))
1929            line = m.group(3)
1930
1931            ## Determine whether this line is flowed
1932            #
1933            if line and line != '-- ' and line[-1] == ' ':
1934                flowed = 1
1935            else:
1936                flowed = 0
1937
1938            if flowed and delsp and line and line[-1] == ' ':
1939                line = line[:-1]
1940
1941            ## If the previous line is flowed, append this line to it
1942            #
1943            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1944                flowedlines[-1] += line
1945
1946            ## Otherwise, start a new line
1947            #
1948            else:
1949                flowedlines.append('>' * linequotelevel + line)
1950
1951            prevflowed = flowed
1952           
1953
1954        return '\n'.join(flowedlines)
1955
1956    def strip_quotes(self, text):
1957        """
1958        Strip quotes from message by Nicolas Mendoza
1959        """
1960        self.logger.debug('function strip_quotes: %s' %self.parameters.email_quote)
1961
1962        body = []
1963
1964        STRIP_RE = re.compile( self.parameters.email_quote )
1965
1966        for line in text.splitlines():
1967
1968            try:
1969
1970                match = STRIP_RE.match(line)
1971                if match:
1972                    print line
1973                    continue
1974
1975            except UnicodeDecodeError:
1976
1977                tmp_line = self.email_to_unicode(line)
1978
1979                match = STRIP_RE.match(tmp_line)
1980                if match:
1981                    print line
1982                    continue
1983               
1984            body.append(line)
1985
1986        return ('\n'.join(body))
1987
1988    def inline_properties(self, text):
1989        """
1990        Parse text if we use inline keywords to set ticket fields
1991        """
1992        self.logger.debug('function inline_properties')
1993
1994        properties = dict()
1995        body = list()
1996
1997        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
1998
1999        for line in text.splitlines():
2000            match = INLINE_EXP.match(line)
2001            if match:
2002                keyword, value = match.groups()
2003
2004                if self.parameters.inline_properties_first_wins:
2005                    if keyword in self.properties.keys():
2006                        continue
2007
2008                self.properties[keyword] = value.strip()
2009                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
2010
2011            else:
2012                body.append(line)
2013               
2014        return '\n'.join(body)
2015
2016
2017    def wrap_text(self, text, replace_whitespace = False):
2018        """
2019        Will break a lines longer then given length into several small
2020        lines of size given length
2021        """
2022        import textwrap
2023
2024        LINESEPARATOR = '\n'
2025        reformat = ''
2026
2027        for s in text.split(LINESEPARATOR):
2028            tmp = textwrap.fill(s, self.parameters.use_textwrap)
2029            if tmp:
2030                reformat = '%s\n%s' %(reformat,tmp)
2031            else:
2032                reformat = '%s\n' %reformat
2033
2034        return reformat
2035
2036        # Python2.4 and higher
2037        #
2038        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
2039        #
2040
2041########## EMAIL attachements functions ###########################################################
2042
2043    def inline_part(self, part):
2044        """
2045        """
2046        self.logger.debug('function inline_part()')
2047
2048        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
2049
2050    def get_message_parts(self, msg, new_email=False):
2051        """
2052        parses the email message and returns a list of body parts and attachments
2053        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
2054        """
2055        self.logger.debug('function get_message_parts()')
2056
2057        message_parts = list()
2058   
2059        ALTERNATIVE_MULTIPART = False
2060
2061        for part in msg.walk():
2062            content_maintype = part.get_content_maintype()
2063            content_type =  part.get_content_type()
2064
2065            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
2066            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
2067
2068            ## Check content type
2069            #
2070            if content_type in self.STRIP_CONTENT_TYPES:
2071                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
2072                continue
2073
2074            ## Catch some mulitpart execptions
2075            #
2076            if content_type == 'multipart/alternative':
2077                ALTERNATIVE_MULTIPART = True
2078                continue
2079
2080            ## Skip multipart containers
2081            #
2082            if content_maintype == 'multipart':
2083                self.logger.debug("\t Skipping multipart container")
2084                continue
2085           
2086            ## Check if this is an inline part. It's inline if there is co Cont-Disp header,
2087            #  or if there is one and it says "inline"
2088            #
2089            inline = self.inline_part(part)
2090
2091            ## Drop HTML message
2092            #
2093            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
2094
2095                if content_type == 'text/html':
2096                    self.logger.debug('\t Skipping alternative HTML message')
2097                    ALTERNATIVE_MULTIPART = False
2098                    continue
2099
2100            filename = part.get_filename()
2101            s = '\t unicode filename: %s' %(filename)
2102            self.print_unicode(s)
2103            self.logger.debug('\t raw filename: %s' %repr(filename))
2104
2105            filename = self.check_filename_length(filename)
2106
2107            ## Save all non plain text message as attachment
2108            #
2109            if not content_type in ['text/plain']:
2110
2111                message_parts.append( (filename, part) )
2112
2113                ## We only convert html messages
2114                #
2115                if not content_type == 'text/html':
2116                    self.logger.debug('\t Appending %s (%s)' %(repr(filename), content_type))
2117                    continue
2118
2119
2120            ## We have an text or html message
2121            #
2122            if not inline:
2123                    self.logger.debug('\t Appending %s (%s), not an inline messsage part' %(repr(filename), content_type))
2124                    message_parts.append( (filename, part) )
2125                    continue
2126               
2127            ## Try to decode message part. We have a html or plain text messafe
2128            #
2129            body_text = part.get_payload(decode=1)
2130            if not body_text:           
2131                body_text = part.get_payload(decode=0)
2132
2133            ## Try to convert html message
2134            #
2135            if content_type == 'text/html':
2136
2137                body_text = self.html_2_txt(body_text)
2138                if not body_text:
2139                    continue
2140
2141            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
2142            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
2143
2144            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
2145                body_text = self.reflow(body_text, delsp == 'yes')
2146
2147            if new_email and self.parameters.only_strip_on_update:
2148                self.logger.debug('Skip signature/quote stripping for new messages')
2149            else:
2150                if self.parameters.strip_signature:
2151                    body_text = self.strip_signature(body_text)
2152
2153                if self.parameters.strip_quotes:
2154                    body_text = self.strip_quotes(body_text)
2155
2156            if self.parameters.inline_properties:
2157                body_text = self.inline_properties(body_text)
2158
2159            if self.parameters.use_textwrap:
2160                body_text = self.wrap_text(body_text)
2161
2162            ## Get contents charset (iso-8859-15 if not defined in mail headers)
2163            #
2164            charset = part.get_content_charset()
2165            if not charset:
2166                charset = 'iso-8859-15'
2167
2168            try:
2169                ubody_text = unicode(body_text, charset)
2170
2171            except UnicodeError, detail:
2172                ubody_text = unicode(body_text, 'iso-8859-15')
2173
2174            except LookupError, detail:
2175                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
2176
2177            if self.parameters.verbatim_format:
2178                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
2179            else:
2180                message_parts.append('%s' %ubody_text)
2181
2182        return message_parts
2183       
2184    def unique_attachment_names(self, message_parts):
2185        """
2186        Make sure we have unique names attachments:
2187          - check if it contains illegal characters
2188          - Rename "None" filenames to "untitled-part"
2189        """
2190        self.logger.debug('function unique_attachment_names()')
2191        renamed_parts = []
2192        attachment_names = set()
2193
2194        for item in message_parts:
2195           
2196            ## If not an attachment, leave it alone
2197            #
2198            if not isinstance(item, tuple):
2199                renamed_parts.append(item)
2200                continue
2201               
2202            (filename, part) = item
2203
2204            ## If filename = None, use a default one
2205            #
2206            if filename in [ 'None']:
2207                filename = 'untitled-part'
2208                self.logger.info('\t Rename filename "None" to: %s' %filename)
2209
2210                ## Guess the extension from the content type, use non strict mode
2211                #  some additional non-standard but commonly used MIME types
2212                #  are also recognized
2213                #
2214                ext = mimetypes.guess_extension(part.get_content_type(), False)
2215                if not ext:
2216                    ext = '.bin'
2217
2218                filename = '%s%s' % (filename, ext)
2219
2220            ## Discard relative paths for windows/unix in attachment names
2221            #
2222            filename = filename.replace('\\', '_')
2223            filename = filename.replace('/', '_')
2224
2225            ## remove linefeed char
2226            #
2227            for forbidden_char in ['\r', '\n']:
2228                filename = filename.replace(forbidden_char,'')
2229
2230            ## We try to normalize the filename to utf-8 NFC if we can.
2231            #  Files uploaded from OS X might be in NFD.
2232            #  Check python version and then try it
2233            #
2234            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2235            #   try:
2236            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2237            #   except TypeError:
2238            #       pass
2239
2240            ## Make the filename unique for this ticket
2241            #
2242            num = 0
2243            unique_filename = filename
2244            dummy_filename, ext = os.path.splitext(filename)
2245
2246            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2247                num += 1
2248                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2249               
2250            s = '\t Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2251            self.print_unicode(s)
2252
2253            attachment_names.add(unique_filename)
2254
2255            renamed_parts.append((filename, unique_filename, part))
2256   
2257        return renamed_parts
2258           
2259           
2260    def attachment_exists(self, filename):
2261
2262        self.logger.debug("function attachment_exists")
2263
2264        s = '\t check if attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2265        self.print_unicode(s)
2266
2267        ## Do we have a valid ticket id
2268        #
2269        if not self.id:
2270            return False
2271
2272        try:
2273            if self.system == 'discussion':
2274
2275                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s' % (self.id,), filename)
2276
2277            elif self.system == 'blog':
2278
2279                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,), filename)
2280
2281            else:
2282
2283                att = attachment.Attachment(self.env, 'ticket', self.id, filename)
2284
2285            return True
2286
2287        except attachment.ResourceNotFound:
2288
2289            return False
2290
2291########## TRAC Ticket Text ###########################################################
2292           
2293    def get_body_text(self, message_parts):
2294        """
2295        """
2296        self.logger.debug('function get_body_text()')
2297
2298        body_text = []
2299       
2300        for part in message_parts:
2301       
2302            ## Plain text part, append it
2303            #
2304            if not isinstance(part, tuple):
2305                body_text.extend(part.strip().splitlines())
2306                body_text.append("")
2307                continue
2308
2309            (original, filename, part) = part
2310            inline = self.inline_part(part)
2311
2312            ## Skip generation of attachment link if html is converted to text
2313            #
2314            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2315                s = 'Skipping attachment link for html part: %s' %(filename)
2316                self.print_unicode(s)
2317                continue
2318           
2319            if part.get_content_maintype() == 'image' and inline:
2320
2321                if self.system != 'discussion':
2322                    s = 'wiki image link for: %s' %(filename)
2323                    self.print_unicode(s)
2324                    body_text.append('[[Image(%s)]]' % filename)
2325
2326                body_text.append("")
2327
2328            else:
2329
2330                if self.system != 'discussion':
2331
2332                    s = 'wiki attachment link for: %s' %(filename)
2333                    self.print_unicode(s)
2334                    body_text.append('[attachment:"%s"]' % filename)
2335
2336                body_text.append("")
2337
2338        ## Convert list body_texts to string
2339        #
2340        body_text = '\r\n'.join(body_text)
2341        return body_text
2342
2343    def html_mailto_link(self, subject):
2344        """
2345        This function returns a HTML mailto tag with the ticket id and author email address
2346        """
2347        self.logger.debug("function html_mailto_link")
2348
2349        if not self.author:
2350            author = self.email_addr
2351        else:   
2352            author = self.author
2353
2354        if not self.parameters.mailto_cc:
2355            self.parameters.mailto_cc = ''
2356           
2357        ## Bug in urllib.quote function
2358        #
2359        if isinstance(subject, unicode):
2360            subject = subject.encode('utf-8')
2361
2362        ## use urllib to escape the chars
2363        #
2364        s = '%s?Subject=%s&cc=%s' %(
2365               urllib.quote(self.email_addr),
2366               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2367               urllib.quote(self.parameters.mailto_cc)
2368               )
2369
2370        if self.VERSION in [ 0.10 ]:
2371            s = '\r\n{{{\r\n#!html\r\n<a\r\n href="mailto:%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
2372        else:
2373            s = '[mailto:"%s" Reply to: %s]' %(s, author)
2374
2375        self.logger.debug("\tmailto link %s" %s)
2376        return s
2377
2378########## TRAC notify section ###########################################################
2379
2380    def notify(self, tkt, new=True, modtime=0):
2381        """
2382        A wrapper for the TRAC notify function. So we can use templates
2383        """
2384        self.logger.debug('function notify()')
2385
2386        if self.parameters.notify_reporter:
2387            self.logger.debug('\t Notify reporter set')
2388            global sender_email
2389            sender_email = self.email_addr
2390 
2391            self.logger.debug('\t Using Email2TracNotification function AlwaysNotifyReporter')
2392            import trac.notification as Email2TracNotification
2393            Email2TracNotification.Notify.notify = AlwaysNotifyReporter
2394
2395        if self.parameters.dry_run  :
2396                self.logger.info('DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter'])
2397                return
2398        try:
2399
2400            #from trac.ticket.notification import TicketNotificationSystem
2401            #tn_sys = TicketNotificationSystem(self.env)
2402            #print tn_sys
2403            #print tn_sys.__dict__
2404            #sys.exit(0)
2405
2406            ## create false {abs_}href properties, to trick Notify()
2407            #
2408            if self.VERSION in [0.10]:
2409                self.env.abs_href = Href(self.get_config('project', 'url'))
2410                self.env.href = Href(self.get_config('project', 'url'))
2411
2412            tn = TicketNotifyEmail(self.env)
2413
2414            if self.parameters.alternate_notify_template:
2415
2416                if self.VERSION >= 0.11:
2417
2418                    from trac.web.chrome import Chrome
2419
2420                    if  self.parameters.alternate_notify_template_update and not new:
2421                        tn.template_name = self.parameters.alternate_notify_template_update
2422                    else:
2423                        tn.template_name = self.parameters.alternate_notify_template
2424
2425                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2426                       
2427                else:
2428
2429                    tn.template_name = self.parameters.alternate_notify_template
2430
2431            tn.notify(tkt, new, modtime)
2432
2433        except Exception, e:
2434            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
2435
2436########## END Class Definition  ########################################################
2437
2438########## Global Notificaition Function ################################################
2439def AlwaysNotifyReporter(self, resid):
2440    """
2441    Copy of def notify() to manipulate recipents to always include reporter for the
2442    notification.
2443    """
2444    #print sender_email, resid
2445    (torcpts, ccrcpts) = self.get_recipients(resid)
2446    #print "get_recipients finished"
2447
2448    if not tktparser.email_header_acl('notify_reporter_black_list', sender_email, False):
2449        ## additionally append sender (regardeless of settings in trac.ini)
2450        #
2451        if not sender_email in torcpts:
2452            torcpts.append(sender_email)
2453
2454    self.begin_send()
2455    self.send(torcpts, ccrcpts)
2456    self.finish_send()
2457
2458########## Parse Config File  ###########################################################
2459
2460def ReadConfig(file, name):
2461    """
2462    Parse the config file
2463    """
2464    if not os.path.isfile(file):
2465        print 'File %s does not exist' %file
2466        sys.exit(1)
2467
2468    config = trac_config.Configuration(file)
2469   
2470    parentdir = config.get('DEFAULT', 'parentdir')
2471    sections = config.sections()
2472
2473    ## use some trac internals to get the defaults
2474    #
2475    tmp = config.parser.defaults()
2476    project =  SaraDict()
2477
2478    for option, value in tmp.items():
2479        try:
2480            project[option] = int(value)
2481        except ValueError:
2482            project[option] = value
2483
2484    if name:
2485        if name in sections:
2486            project =  SaraDict()
2487            for option, value in  config.options(name):
2488                try:
2489                    project[option] = int(value)
2490                except ValueError:
2491                    project[option] = value
2492
2493        elif not parentdir:
2494            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2495            print "or set parentdir in the [DEFAULT] section"
2496            sys.exit(1)
2497
2498    ## If parentdir then set project dir to parentdir + name
2499    #
2500    if not project.has_key('project'):
2501        if not parentdir:
2502            print "You must set project or parentdir in your configuration file"
2503            sys.exit(1)
2504        elif not name:
2505            print "You must configure a  project section in your configuration file"
2506        else:
2507            project['project'] = os.path.join(parentdir, name)
2508
2509    ##
2510    # Save the project name
2511    #
2512    project['project_name'] = os.path.basename(project['project'])
2513
2514    return project
2515
2516########## Setup Logging ###############################################################
2517
2518def setup_log(parameters, project_name, interactive=None):
2519    """
2520    Setup logging
2521
2522    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
2523    would be interpreted by the ConfigParser itself.
2524    """
2525    logger = logging.getLogger('email2trac %s' %project_name)
2526
2527    if interactive:
2528        parameters.log_type = 'stderr'
2529
2530    if not parameters.log_type:
2531        if sys.platform in ['win32', 'cygwin']:
2532            parameters.log_type = 'eventlog'
2533        else:
2534            parameters.log_type = 'syslog'
2535
2536    if parameters.log_type == 'file':
2537
2538        if not parameters.log_file:
2539            parameters.log_file = 'email2trac.log'
2540
2541        if not os.path.isabs(parameters.log_file):
2542            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
2543
2544        log_handler = logging.FileHandler(parameters.log_file)
2545
2546    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2547        ## Requires win32 extensions
2548        #
2549        logid = "email2trac"
2550        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
2551
2552    elif parameters.log_type in ('syslog', 'unix'):
2553        log_handler = logging.handlers.SysLogHandler('/dev/log')
2554
2555    elif parameters.log_type in ('stderr'):
2556        log_handler = logging.StreamHandler(sys.stderr)
2557
2558    else:
2559        log_handler = logging.handlers.BufferingHandler(0)
2560
2561    if parameters.log_format:
2562        parameters.log_format = parameters.log_format.replace('$(', '%(')
2563    else:
2564        parameters.log_format = '%(name)s: %(message)s'
2565        if parameters.log_type in ('file', 'stderr'):
2566            parameters.log_format = '%(asctime)s ' + parameters.log_format
2567
2568    log_formatter = logging.Formatter(parameters.log_format)
2569    log_handler.setFormatter(log_formatter)
2570    logger.addHandler(log_handler)
2571
2572    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2573        logger.setLevel(logging.DEBUG)
2574        parameters.debug = 1
2575
2576    elif parameters.log_level in ['INFO'] or parameters.verbose:
2577        logger.setLevel(logging.INFO)
2578
2579    elif parameters.log_level in ['WARNING']:
2580        logger.setLevel(logging.WARNING)
2581
2582    elif parameters.log_level in ['ERROR']:
2583        logger.setLevel(logging.ERROR)
2584
2585    elif parameters.log_level in ['CRITICAL']:
2586        logger.setLevel(logging.CRITICAL)
2587
2588    else:
2589        logger.setLevel(logging.INFO)
2590
2591    return logger
2592
2593
2594if __name__ == '__main__':
2595    ## Default config file
2596    #
2597    configfile = '@email2trac_conf@'
2598    project = ''
2599    component = ''
2600    ticket_prefix = 'default'
2601    dry_run = None
2602    verbose = None
2603    debug_interactive = None
2604    virtualenv = '@virtualenv@'
2605
2606    SHORT_OPT = 'cde:hf:np:t:v'
2607    LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'virtualenv=', 'verbose']
2608
2609    try:
2610        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2611    except getopt.error,detail:
2612        print __doc__
2613        print detail
2614        sys.exit(1)
2615   
2616    project_name = None
2617    for opt,value in opts:
2618        if opt in [ '-h', '--help']:
2619            print __doc__
2620            sys.exit(0)
2621        elif opt in ['-c', '--component']:
2622            component = value
2623        elif opt in ['-d', '--debug']:
2624            debug_interactive = 1
2625        elif opt in ['-e', '--virtualenv']:
2626            virtualenv = value
2627        elif opt in ['-f', '--file']:
2628            configfile = value
2629        elif opt in ['-n', '--dry-run']:
2630            dry_run = True
2631        elif opt in ['-p', '--project']:
2632            project_name = value
2633        elif opt in ['-t', '--ticket_prefix']:
2634            ticket_prefix = value
2635        elif opt in ['-v', '--verbose']:
2636            verbose = True
2637
2638    if virtualenv and os.path.exists(virtualenv):
2639        activate_this = os.path.join(virtualenv, 'bin/activate_this.py')
2640        if os.path.exists(activate_this):
2641            execfile(activate_this, dict(__file__=activate_this))
2642
2643    try:
2644        from trac import __version__ as trac_version
2645        from trac import config as trac_config
2646
2647    except ImportError, detail:
2648        print "Can not find a a valid trac installation, solutions could be:"
2649        print "\tset PYTHONPATH"
2650        print "\tuse the --virtualenv <dir> option"
2651        sys.exit(1)
2652   
2653    settings = ReadConfig(configfile, project_name)
2654
2655    ## The default prefix for ticket values in email2trac.conf
2656    #
2657    settings.ticket_prefix = ticket_prefix
2658    settings.dry_run = dry_run
2659    settings.verbose = verbose
2660
2661    if not settings.debug and debug_interactive:
2662        settings.debug = debug_interactive
2663
2664    if not settings.project:
2665        print __doc__
2666        print 'No Trac project is defined in the email2trac config file.'
2667        sys.exit(1)
2668
2669    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2670   
2671    if component:
2672        settings['component'] = component
2673
2674    ## We are only interested in the major versions
2675    # 0.12.3 --> 0.12
2676    # 1.0.2  --> 1.0
2677    #
2678    l = trac_version.split('.')
2679    version = '.'.join(l[0:2])
2680
2681    logger.debug("Found trac version: %s" %(version))
2682   
2683    try:
2684        if version == '0.10':
2685            from trac import attachment
2686            from trac.env import Environment
2687            from trac.ticket import Ticket
2688            from trac.web.href import Href
2689            from trac import util
2690            from trac.ticket.web_ui import TicketModule
2691
2692            #
2693            # return  util.text.to_unicode(str)
2694            #
2695            # see http://projects.edgewall.com/trac/changeset/2799
2696            from trac.ticket.notification import TicketNotifyEmail
2697            from trac import config as trac_config
2698            from trac.core import TracError
2699
2700        elif version in ['0.11', '0.12', '0.13', '1.0']:
2701            from trac import attachment
2702            from trac import config as trac_config
2703            from trac import util
2704            from trac.core import TracError
2705            from trac.env import Environment
2706            from trac.perm import PermissionSystem
2707            from trac.perm import PermissionCache
2708            from trac.test import Mock, MockPerm
2709            from trac.ticket import Ticket
2710            from trac.ticket.api import TicketSystem
2711            from trac.ticket.web_ui import TicketModule
2712            from trac.web.href import Href
2713
2714            #
2715            # return  util.text.to_unicode(str)
2716            #
2717            # see http://projects.edgewall.com/trac/changeset/2799
2718            from trac.ticket.notification import TicketNotifyEmail
2719
2720        else:
2721            logger.error('TRAC version %s is not supported' %version)
2722            sys.exit(0)
2723
2724        ## Must be set before environment is created
2725        #
2726        if settings.has_key('python_egg_cache'):
2727            python_egg_cache = str(settings['python_egg_cache'])
2728            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2729
2730        if settings.debug > 0:
2731            logger.debug('Loading environment %s', settings.project)
2732
2733        try:
2734            env = Environment(settings['project'], create=0)
2735        except IOError, detail:
2736            logger.error("trac error: %s" %detail)
2737            sys.exit(0)
2738        except TracError, detail:
2739            logger.error("trac error: %s" %detail)
2740            sys.exit(0)
2741
2742        tktparser = TicketEmailParser(env, settings, logger, float(version))
2743        tktparser.parse(sys.stdin)
2744
2745    ## Catch all errors and use the logging module
2746    #
2747    except Exception, error:
2748
2749        etype, evalue, etb = sys.exc_info()
2750        for e in traceback.format_exception(etype, evalue, etb):
2751            logger.critical(e)
2752
2753        if m:
2754            tktparser.save_email_for_debug(m, settings.project_name, True)
2755
2756        sys.exit(1)
2757# EOB
Note: See TracBrowser for help on using the repository browser.