| 66 | | self.config = self.env.config |
|---|
| 67 | | self.encoding = 'utf-8' |
|---|
| 68 | | subject = 'Ticket Summary for %s' % self.client |
|---|
| 69 | | |
|---|
| 70 | | if not self.config.getbool('notification', 'smtp_enabled'): |
|---|
| 71 | | return False |
|---|
| 72 | | smtp_server = self.config['notification'].get('smtp_server') |
|---|
| 73 | | smtp_port = self.config['notification'].getint('smtp_port') |
|---|
| 74 | | from_email = self.config['notification'].get('smtp_from') |
|---|
| 75 | | from_name = self.config['notification'].get('smtp_from_name') |
|---|
| 76 | | replyto_email = self.config['notification'].get('smtp_replyto') |
|---|
| 77 | | from_email = from_email or replyto_email |
|---|
| 78 | | if not from_email: |
|---|
| 79 | | return False |
|---|
| 80 | | |
|---|
| 81 | | # Authentication info (optional) |
|---|
| 82 | | user_name = self.config['notification'].get('smtp_user') |
|---|
| 83 | | password = self.config['notification'].get('smtp_password') |
|---|
| 84 | | |
|---|
| 85 | | # Thanks to the author of this recipe: |
|---|
| 86 | | # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473810 |
|---|
| 87 | | |
|---|
| 88 | | from email.MIMEMultipart import MIMEMultipart |
|---|
| 89 | | from email.MIMEText import MIMEText |
|---|
| 90 | | from email.MIMEImage import MIMEImage |
|---|
| 91 | | from email.Charset import add_charset, SHORTEST |
|---|
| 92 | | add_charset( 'utf-8', SHORTEST, None, None ) |
|---|
| 93 | | |
|---|
| 94 | | projname = self.config.get('project', 'name') |
|---|
| 95 | | |
|---|
| 96 | | # Create the root message and fill in the from, to, and subject headers |
|---|
| 97 | | msg_root = MIMEMultipart('alternative') |
|---|
| 98 | | msg_root['To'] = str(', ').join(self.emails) |
|---|
| 99 | | |
|---|
| 100 | | msg_root['X-Mailer'] = 'ClientsPlugin for Trac' |
|---|
| 101 | | #msg_root['X-Trac-Version'] = __version__ |
|---|
| 102 | | msg_root['X-Trac-Project'] = projname |
|---|
| 103 | | msg_root['Precedence'] = 'bulk' |
|---|
| 104 | | msg_root['Auto-Submitted'] = 'auto-generated' |
|---|
| 105 | | msg_root['Subject'] = subject |
|---|
| 106 | | msg_root['From'] = '%s <%s>' % (from_name or projname, from_email) |
|---|
| 107 | | msg_root['Reply-To'] = replyto_email |
|---|
| 108 | | msg_root.preamble = 'This is a multi-part message in MIME format.' |
|---|
| 109 | | |
|---|
| 110 | | view = 'plain' |
|---|
| 111 | | arg = "'%s'" % view |
|---|
| 112 | | result = self.transform(summary, view=arg) |
|---|
| 113 | | msg_text = MIMEText(str(result), view, self.encoding) |
|---|
| 114 | | msg_root.attach(msg_text) |
|---|
| 115 | | |
|---|
| 116 | | msg_related = MIMEMultipart('related') |
|---|
| 117 | | msg_root.attach(msg_related) |
|---|
| 118 | | |
|---|
| 119 | | view = 'html' |
|---|
| 120 | | arg = "'%s'" % view |
|---|
| 121 | | result = self.transform(summary, view=arg) |
|---|
| 122 | | #file = open('/tmp/send-client-email.html', 'w') |
|---|
| 123 | | #file.write(str(result)) |
|---|
| 124 | | #file.close() |
|---|
| 125 | | |
|---|
| 126 | | msg_text = MIMEText(str(result), view, self.encoding) |
|---|
| 127 | | msg_related.attach(msg_text) |
|---|
| 128 | | |
|---|
| 129 | | # Handle image embedding... |
|---|
| 130 | | view = 'images' |
|---|
| 131 | | arg = "'%s'" % view |
|---|
| 132 | | result = self.transform(summary, view=arg) |
|---|
| 133 | | if result: |
|---|
| 134 | | images = result.getroot() |
|---|
| 135 | | if images: |
|---|
| 136 | | for img in images: |
|---|
| 137 | | if 'img' != img.tag: |
|---|
| 138 | | continue |
|---|
| 139 | | if not img.get('id') or not img.get('src'): |
|---|
| 140 | | continue |
|---|
| 141 | | |
|---|
| 142 | | fp = open(img.get('src'), 'rb') |
|---|
| 143 | | if not fp: |
|---|
| 144 | | continue |
|---|
| 145 | | msg_img = MIMEImage(fp.read()) |
|---|
| 146 | | fp.close() |
|---|
| 147 | | msg_img.add_header('Content-ID', '<%s>' % img.get('id')) |
|---|
| 148 | | msg_related.attach(msg_img) |
|---|
| 149 | | |
|---|
| 150 | | # Send the email |
|---|
| 151 | | import smtplib |
|---|
| 152 | | smtp = smtplib.SMTP() #smtp_server, smtp_port) |
|---|
| 153 | | if False and user_name: |
|---|
| 154 | | smtp.login(user_name, password) |
|---|
| 155 | | smtp.connect() |
|---|
| 156 | | smtp.sendmail(from_email, self.emails, msg_root.as_string()) |
|---|
| 157 | | smtp.quit() |
|---|
| | 98 | |
|---|
| | 99 | result = self.transform(summary) |
|---|
| | 100 | |
|---|
| | 101 | username = self.username |
|---|
| | 102 | password = self.password |
|---|
| | 103 | uri = self.uri |
|---|
| | 104 | |
|---|
| | 105 | host, port, path = parseuri(uri) |
|---|
| | 106 | |
|---|
| | 107 | redirect = set([301, 302, 307]) |
|---|
| | 108 | authenticate = set([401]) |
|---|
| | 109 | okay = set([200, 201, 204]) |
|---|
| | 110 | |
|---|
| | 111 | authorized = False |
|---|
| | 112 | authorization = None |
|---|
| | 113 | tries = 0 |
|---|
| | 114 | verbose = True |
|---|
| | 115 | |
|---|
| | 116 | while True: |
|---|
| | 117 | # Attempt to HTTP PUT the data |
|---|
| | 118 | h = httplib.HTTPConnection(host, port) |
|---|
| | 119 | |
|---|
| | 120 | h.putrequest('PUT', path) |
|---|
| | 121 | |
|---|
| | 122 | h.putheader('User-Agent', 'Trac/1.0') |
|---|
| | 123 | h.putheader('Connection', 'keep-alive') |
|---|
| | 124 | h.putheader('Transfer-Encoding', 'chunked') |
|---|
| | 125 | h.putheader('Expect', '100-continue') |
|---|
| | 126 | h.putheader('Accept', 'application/xml') |
|---|
| | 127 | h.putheader('Content-Type', 'text/xml') |
|---|
| | 128 | h.putheader('Content-Length', len(str(result))) |
|---|
| | 129 | if authorization: |
|---|
| | 130 | h.putheader('Authorization', authorization) |
|---|
| | 131 | h.endheaders() |
|---|
| | 132 | |
|---|
| | 133 | # Chunked transfer encoding |
|---|
| | 134 | # Cf. 'All HTTP/1.1 applications MUST be able to receive and |
|---|
| | 135 | # decode the "chunked" transfer-coding' |
|---|
| | 136 | # - http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html |
|---|
| | 137 | while True: |
|---|
| | 138 | #bytes = f.read(2048) |
|---|
| | 139 | bytes = str(result) |
|---|
| | 140 | if not bytes: break |
|---|
| | 141 | |
|---|
| | 142 | length = len(bytes) |
|---|
| | 143 | h.send('%X\r\n' % length) |
|---|
| | 144 | h.send(bytes + '\r\n') |
|---|
| | 145 | break |
|---|
| | 146 | h.send('0\r\n\r\n') |
|---|
| | 147 | |
|---|
| | 148 | resp = h.getresponse() |
|---|
| | 149 | status = resp.status # an int |
|---|
| | 150 | |
|---|
| | 151 | # Got a response, now decide how to act upon it |
|---|
| | 152 | if status in redirect: |
|---|
| | 153 | location = resp.getheader('Location') |
|---|
| | 154 | uri = urlparse.urljoin(uri, location) |
|---|
| | 155 | host, port, path = parseuri(uri) |
|---|
| | 156 | |
|---|
| | 157 | # We may have to authenticate again |
|---|
| | 158 | if authorization: |
|---|
| | 159 | authorization = None |
|---|
| | 160 | |
|---|
| | 161 | elif status in authenticate: |
|---|
| | 162 | # If we've done this already, break |
|---|
| | 163 | if authorization: |
|---|
| | 164 | # barf("Going around in authentication circles") |
|---|
| | 165 | print "Authentication failed" |
|---|
| | 166 | return False |
|---|
| | 167 | |
|---|
| | 168 | if not (username and password): |
|---|
| | 169 | print "Need a username and password to authenticate with" |
|---|
| | 170 | return False |
|---|
| | 171 | |
|---|
| | 172 | # Get the scheme: Basic or Digest? |
|---|
| | 173 | wwwauth = resp.msg['www-authenticate'] # We may need this again |
|---|
| | 174 | wauth = wwwauth.lstrip(' \t') # Hence use wauth not wwwauth here |
|---|
| | 175 | wauth = wwwauth.replace('\t', ' ') |
|---|
| | 176 | i = wauth.index(' ') |
|---|
| | 177 | scheme = wauth[:i].lower() |
|---|
| | 178 | |
|---|
| | 179 | if scheme in set(['basic', 'digest']): |
|---|
| | 180 | if verbose: |
|---|
| | 181 | msg = "Performing %s Authentication..." % scheme.capitalize() |
|---|
| | 182 | print >> sys.stderr, msg |
|---|
| | 183 | else: |
|---|
| | 184 | print "Unknown authentication scheme: %s" % scheme |
|---|
| | 185 | return False |
|---|
| | 186 | |
|---|
| | 187 | if scheme == 'basic': |
|---|
| | 188 | import base64 |
|---|
| | 189 | userpass = username + ':' + password |
|---|
| | 190 | userpass = base64.encodestring(userpass).strip() |
|---|
| | 191 | authorized, authorization = True, 'Basic ' + userpass |
|---|
| | 192 | |
|---|
| | 193 | elif scheme == 'digest': |
|---|
| | 194 | if verbose: |
|---|
| | 195 | msg = "uses fragile, undocumented features in urllib2" |
|---|
| | 196 | print >> sys.stderr, "Warning! Digest Auth %s" % msg |
|---|
| | 197 | |
|---|
| | 198 | import urllib2 # See warning above |
|---|
| | 199 | |
|---|
| | 200 | passwd = type('Password', (object,), { |
|---|
| | 201 | 'find_user_password': lambda self, *args: (username, password), |
|---|
| | 202 | 'add_password': lambda self, *args: None |
|---|
| | 203 | })() |
|---|
| | 204 | |
|---|
| | 205 | xreq = type('Request', (object,), { |
|---|
| | 206 | 'get_full_url': lambda self: uri, |
|---|
| | 207 | 'has_data': lambda self: None, |
|---|
| | 208 | 'get_method': lambda self: 'PUT', |
|---|
| | 209 | 'get_selector': lambda self: path |
|---|
| | 210 | })() |
|---|
| | 211 | |
|---|
| | 212 | # Cf. urllib2.AbstractDigestAuthHandler.retry_http_digest_auth |
|---|
| | 213 | auth = urllib2.AbstractDigestAuthHandler(passwd) |
|---|
| | 214 | token, challenge = wwwauth.split(' ', 1) |
|---|
| | 215 | chal = urllib2.parse_keqv_list(urllib2.parse_http_list(challenge)) |
|---|
| | 216 | userpass = auth.get_authorization(xreq, chal) |
|---|
| | 217 | authorized, authorization = True, 'Digest ' + userpass |
|---|
| | 218 | |
|---|
| | 219 | elif status in okay: |
|---|
| | 220 | if (username and password) and (not authorized): |
|---|
| | 221 | msg = "Warning! The supplied username and password went unused" |
|---|
| | 222 | print >> sys.stderr, msg |
|---|
| | 223 | |
|---|
| | 224 | if verbose: |
|---|
| | 225 | resultLine = "Success! Resource %s" |
|---|
| | 226 | statuses = {200: 'modified', 201: 'created', 204: 'modified'} |
|---|
| | 227 | print resultLine % statuses[status] |
|---|
| | 228 | |
|---|
| | 229 | statusLine = "Response-Status: %s %s" |
|---|
| | 230 | print statusLine % (status, resp.reason) |
|---|
| | 231 | |
|---|
| | 232 | body = resp.read(58) |
|---|
| | 233 | body = body.rstrip('\r\n') |
|---|
| | 234 | body = body.encode('string_escape') |
|---|
| | 235 | |
|---|
| | 236 | if len(body) >= 58: |
|---|
| | 237 | body = body[:57] + '[...]' |
|---|
| | 238 | |
|---|
| | 239 | bodyLine = 'Response-Body: "%s"' |
|---|
| | 240 | print bodyLine % body |
|---|
| | 241 | break |
|---|
| | 242 | |
|---|
| | 243 | # @@ raise PutError, do the catching in main? |
|---|
| | 244 | else: |
|---|
| | 245 | print 'Got "%s %s"' % (status, resp.reason) |
|---|
| | 246 | return False |
|---|
| | 247 | |
|---|
| | 248 | tries += 1 |
|---|
| | 249 | if tries >= 50: |
|---|
| | 250 | print "Too many redirects" |
|---|
| | 251 | return False |
|---|
| | 252 | |
|---|
| | 253 | print str(result) |
|---|