Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/mail/message.py: 21%

257 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import mimetypes 

2from email import charset as Charset 

3from email import encoders as Encoders 

4from email import generator, message_from_string 

5from email.errors import HeaderParseError 

6from email.header import Header 

7from email.headerregistry import Address, parser 

8from email.message import Message 

9from email.mime.base import MIMEBase 

10from email.mime.message import MIMEMessage 

11from email.mime.multipart import MIMEMultipart 

12from email.mime.text import MIMEText 

13from email.utils import formataddr, formatdate, getaddresses, make_msgid 

14from io import BytesIO, StringIO 

15from pathlib import Path 

16 

17from django.conf import settings 

18from django.core.mail.utils import DNS_NAME 

19from django.utils.encoding import force_str, punycode 

20 

21# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from 

22# some spam filters. 

23utf8_charset = Charset.Charset("utf-8") 

24utf8_charset.body_encoding = None # Python defaults to BASE64 

25utf8_charset_qp = Charset.Charset("utf-8") 

26utf8_charset_qp.body_encoding = Charset.QP 

27 

28# Default MIME type to use on attachments (if it is not explicitly given 

29# and cannot be guessed). 

30DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream" 

31 

32RFC5322_EMAIL_LINE_LENGTH_LIMIT = 998 

33 

34 

35class BadHeaderError(ValueError): 

36 pass 

37 

38 

39# Header names that contain structured address data (RFC #5322) 

40ADDRESS_HEADERS = { 

41 "from", 

42 "sender", 

43 "reply-to", 

44 "to", 

45 "cc", 

46 "bcc", 

47 "resent-from", 

48 "resent-sender", 

49 "resent-to", 

50 "resent-cc", 

51 "resent-bcc", 

52} 

53 

54 

55def forbid_multi_line_headers(name, val, encoding): 

56 """Forbid multi-line headers to prevent header injection.""" 

57 encoding = encoding or settings.DEFAULT_CHARSET 

58 val = str(val) # val may be lazy 

59 if "\n" in val or "\r" in val: 

60 raise BadHeaderError( 

61 "Header values can't contain newlines (got %r for header %r)" % (val, name) 

62 ) 

63 try: 

64 val.encode("ascii") 

65 except UnicodeEncodeError: 

66 if name.lower() in ADDRESS_HEADERS: 

67 val = ", ".join( 

68 sanitize_address(addr, encoding) for addr in getaddresses((val,)) 

69 ) 

70 else: 

71 val = Header(val, encoding).encode() 

72 else: 

73 if name.lower() == "subject": 

74 val = Header(val).encode() 

75 return name, val 

76 

77 

78def sanitize_address(addr, encoding): 

79 """ 

80 Format a pair of (name, address) or an email address string. 

81 """ 

82 address = None 

83 if not isinstance(addr, tuple): 

84 addr = force_str(addr) 

85 try: 

86 token, rest = parser.get_mailbox(addr) 

87 except (HeaderParseError, ValueError, IndexError): 

88 raise ValueError('Invalid address "%s"' % addr) 

89 else: 

90 if rest: 

91 # The entire email address must be parsed. 

92 raise ValueError( 

93 'Invalid address; only %s could be parsed from "%s"' % (token, addr) 

94 ) 

95 nm = token.display_name or "" 

96 localpart = token.local_part 

97 domain = token.domain or "" 

98 else: 

99 nm, address = addr 

100 localpart, domain = address.rsplit("@", 1) 

101 

102 address_parts = nm + localpart + domain 

103 if "\n" in address_parts or "\r" in address_parts: 

104 raise ValueError("Invalid address; address parts cannot contain newlines.") 

105 

106 # Avoid UTF-8 encode, if it's possible. 

107 try: 

108 nm.encode("ascii") 

109 nm = Header(nm).encode() 

110 except UnicodeEncodeError: 

111 nm = Header(nm, encoding).encode() 

112 try: 

113 localpart.encode("ascii") 

114 except UnicodeEncodeError: 

115 localpart = Header(localpart, encoding).encode() 

116 domain = punycode(domain) 

117 

118 parsed_address = Address(username=localpart, domain=domain) 

119 return formataddr((nm, parsed_address.addr_spec)) 

120 

121 

122class MIMEMixin: 

123 def as_string(self, unixfrom=False, linesep="\n"): 

124 """Return the entire formatted message as a string. 

125 Optional `unixfrom' when True, means include the Unix From_ envelope 

126 header. 

127 

128 This overrides the default as_string() implementation to not mangle 

129 lines that begin with 'From '. See bug #13433 for details. 

130 """ 

131 fp = StringIO() 

132 g = generator.Generator(fp, mangle_from_=False) 

133 g.flatten(self, unixfrom=unixfrom, linesep=linesep) 

134 return fp.getvalue() 

135 

136 def as_bytes(self, unixfrom=False, linesep="\n"): 

137 """Return the entire formatted message as bytes. 

138 Optional `unixfrom' when True, means include the Unix From_ envelope 

139 header. 

140 

141 This overrides the default as_bytes() implementation to not mangle 

142 lines that begin with 'From '. See bug #13433 for details. 

143 """ 

144 fp = BytesIO() 

145 g = generator.BytesGenerator(fp, mangle_from_=False) 

146 g.flatten(self, unixfrom=unixfrom, linesep=linesep) 

147 return fp.getvalue() 

148 

149 

150class SafeMIMEMessage(MIMEMixin, MIMEMessage): 

151 def __setitem__(self, name, val): 

152 # message/rfc822 attachments must be ASCII 

153 name, val = forbid_multi_line_headers(name, val, "ascii") 

154 MIMEMessage.__setitem__(self, name, val) 

155 

156 

157class SafeMIMEText(MIMEMixin, MIMEText): 

158 def __init__(self, _text, _subtype="plain", _charset=None): 

159 self.encoding = _charset 

160 MIMEText.__init__(self, _text, _subtype=_subtype, _charset=_charset) 

161 

162 def __setitem__(self, name, val): 

163 name, val = forbid_multi_line_headers(name, val, self.encoding) 

164 MIMEText.__setitem__(self, name, val) 

165 

166 def set_payload(self, payload, charset=None): 

167 if charset == "utf-8" and not isinstance(charset, Charset.Charset): 

168 has_long_lines = any( 

169 len(line.encode()) > RFC5322_EMAIL_LINE_LENGTH_LIMIT 

170 for line in payload.splitlines() 

171 ) 

172 # Quoted-Printable encoding has the side effect of shortening long 

173 # lines, if any (#22561). 

174 charset = utf8_charset_qp if has_long_lines else utf8_charset 

175 MIMEText.set_payload(self, payload, charset=charset) 

176 

177 

178class SafeMIMEMultipart(MIMEMixin, MIMEMultipart): 

179 def __init__( 

180 self, _subtype="mixed", boundary=None, _subparts=None, encoding=None, **_params 

181 ): 

182 self.encoding = encoding 

183 MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params) 

184 

185 def __setitem__(self, name, val): 

186 name, val = forbid_multi_line_headers(name, val, self.encoding) 

187 MIMEMultipart.__setitem__(self, name, val) 

188 

189 

190class EmailMessage: 

191 """A container for email information.""" 

192 

193 content_subtype = "plain" 

194 mixed_subtype = "mixed" 

195 encoding = None # None => use settings default 

196 

197 def __init__( 

198 self, 

199 subject="", 

200 body="", 

201 from_email=None, 

202 to=None, 

203 bcc=None, 

204 connection=None, 

205 attachments=None, 

206 headers=None, 

207 cc=None, 

208 reply_to=None, 

209 ): 

210 """ 

211 Initialize a single email message (which can be sent to multiple 

212 recipients). 

213 """ 

214 if to: 

215 if isinstance(to, str): 

216 raise TypeError('"to" argument must be a list or tuple') 

217 self.to = list(to) 

218 else: 

219 self.to = [] 

220 if cc: 

221 if isinstance(cc, str): 

222 raise TypeError('"cc" argument must be a list or tuple') 

223 self.cc = list(cc) 

224 else: 

225 self.cc = [] 

226 if bcc: 

227 if isinstance(bcc, str): 

228 raise TypeError('"bcc" argument must be a list or tuple') 

229 self.bcc = list(bcc) 

230 else: 

231 self.bcc = [] 

232 if reply_to: 

233 if isinstance(reply_to, str): 

234 raise TypeError('"reply_to" argument must be a list or tuple') 

235 self.reply_to = list(reply_to) 

236 else: 

237 self.reply_to = [] 

238 self.from_email = from_email or settings.DEFAULT_FROM_EMAIL 

239 self.subject = subject 

240 self.body = body or "" 

241 self.attachments = [] 

242 if attachments: 

243 for attachment in attachments: 

244 if isinstance(attachment, MIMEBase): 

245 self.attach(attachment) 

246 else: 

247 self.attach(*attachment) 

248 self.extra_headers = headers or {} 

249 self.connection = connection 

250 

251 def get_connection(self, fail_silently=False): 

252 from django.core.mail import get_connection 

253 

254 if not self.connection: 

255 self.connection = get_connection(fail_silently=fail_silently) 

256 return self.connection 

257 

258 def message(self): 

259 encoding = self.encoding or settings.DEFAULT_CHARSET 

260 msg = SafeMIMEText(self.body, self.content_subtype, encoding) 

261 msg = self._create_message(msg) 

262 msg["Subject"] = self.subject 

263 msg["From"] = self.extra_headers.get("From", self.from_email) 

264 self._set_list_header_if_not_empty(msg, "To", self.to) 

265 self._set_list_header_if_not_empty(msg, "Cc", self.cc) 

266 self._set_list_header_if_not_empty(msg, "Reply-To", self.reply_to) 

267 

268 # Email header names are case-insensitive (RFC 2045), so we have to 

269 # accommodate that when doing comparisons. 

270 header_names = [key.lower() for key in self.extra_headers] 

271 if "date" not in header_names: 

272 # formatdate() uses stdlib methods to format the date, which use 

273 # the stdlib/OS concept of a timezone, however, Django sets the 

274 # TZ environment variable based on the TIME_ZONE setting which 

275 # will get picked up by formatdate(). 

276 msg["Date"] = formatdate(localtime=settings.EMAIL_USE_LOCALTIME) 

277 if "message-id" not in header_names: 

278 # Use cached DNS_NAME for performance 

279 msg["Message-ID"] = make_msgid(domain=DNS_NAME) 

280 for name, value in self.extra_headers.items(): 

281 if name.lower() != "from": # From is already handled 

282 msg[name] = value 

283 return msg 

284 

285 def recipients(self): 

286 """ 

287 Return a list of all recipients of the email (includes direct 

288 addressees as well as Cc and Bcc entries). 

289 """ 

290 return [email for email in (self.to + self.cc + self.bcc) if email] 

291 

292 def send(self, fail_silently=False): 

293 """Send the email message.""" 

294 if not self.recipients(): 

295 # Don't bother creating the network connection if there's nobody to 

296 # send to. 

297 return 0 

298 return self.get_connection(fail_silently).send_messages([self]) 

299 

300 def attach(self, filename=None, content=None, mimetype=None): 

301 """ 

302 Attach a file with the given filename and content. The filename can 

303 be omitted and the mimetype is guessed, if not provided. 

304 

305 If the first parameter is a MIMEBase subclass, insert it directly 

306 into the resulting message attachments. 

307 

308 For a text/* mimetype (guessed or specified), when a bytes object is 

309 specified as content, decode it as UTF-8. If that fails, set the 

310 mimetype to DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content. 

311 """ 

312 if isinstance(filename, MIMEBase): 

313 if content is not None or mimetype is not None: 

314 raise ValueError( 

315 "content and mimetype must not be given when a MIMEBase " 

316 "instance is provided." 

317 ) 

318 self.attachments.append(filename) 

319 elif content is None: 

320 raise ValueError("content must be provided.") 

321 else: 

322 mimetype = ( 

323 mimetype 

324 or mimetypes.guess_type(filename)[0] 

325 or DEFAULT_ATTACHMENT_MIME_TYPE 

326 ) 

327 basetype, subtype = mimetype.split("/", 1) 

328 

329 if basetype == "text": 

330 if isinstance(content, bytes): 

331 try: 

332 content = content.decode() 

333 except UnicodeDecodeError: 

334 # If mimetype suggests the file is text but it's 

335 # actually binary, read() raises a UnicodeDecodeError. 

336 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE 

337 

338 self.attachments.append((filename, content, mimetype)) 

339 

340 def attach_file(self, path, mimetype=None): 

341 """ 

342 Attach a file from the filesystem. 

343 

344 Set the mimetype to DEFAULT_ATTACHMENT_MIME_TYPE if it isn't specified 

345 and cannot be guessed. 

346 

347 For a text/* mimetype (guessed or specified), decode the file's content 

348 as UTF-8. If that fails, set the mimetype to 

349 DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content. 

350 """ 

351 path = Path(path) 

352 with path.open("rb") as file: 

353 content = file.read() 

354 self.attach(path.name, content, mimetype) 

355 

356 def _create_message(self, msg): 

357 return self._create_attachments(msg) 

358 

359 def _create_attachments(self, msg): 

360 if self.attachments: 

361 encoding = self.encoding or settings.DEFAULT_CHARSET 

362 body_msg = msg 

363 msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding) 

364 if self.body or body_msg.is_multipart(): 

365 msg.attach(body_msg) 

366 for attachment in self.attachments: 

367 if isinstance(attachment, MIMEBase): 

368 msg.attach(attachment) 

369 else: 

370 msg.attach(self._create_attachment(*attachment)) 

371 return msg 

372 

373 def _create_mime_attachment(self, content, mimetype): 

374 """ 

375 Convert the content, mimetype pair into a MIME attachment object. 

376 

377 If the mimetype is message/rfc822, content may be an 

378 email.Message or EmailMessage object, as well as a str. 

379 """ 

380 basetype, subtype = mimetype.split("/", 1) 

381 if basetype == "text": 

382 encoding = self.encoding or settings.DEFAULT_CHARSET 

383 attachment = SafeMIMEText(content, subtype, encoding) 

384 elif basetype == "message" and subtype == "rfc822": 

385 # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments 

386 # must not be base64 encoded. 

387 if isinstance(content, EmailMessage): 

388 # convert content into an email.Message first 

389 content = content.message() 

390 elif not isinstance(content, Message): 

391 # For compatibility with existing code, parse the message 

392 # into an email.Message object if it is not one already. 

393 content = message_from_string(force_str(content)) 

394 

395 attachment = SafeMIMEMessage(content, subtype) 

396 else: 

397 # Encode non-text attachments with base64. 

398 attachment = MIMEBase(basetype, subtype) 

399 attachment.set_payload(content) 

400 Encoders.encode_base64(attachment) 

401 return attachment 

402 

403 def _create_attachment(self, filename, content, mimetype=None): 

404 """ 

405 Convert the filename, content, mimetype triple into a MIME attachment 

406 object. 

407 """ 

408 attachment = self._create_mime_attachment(content, mimetype) 

409 if filename: 

410 try: 

411 filename.encode("ascii") 

412 except UnicodeEncodeError: 

413 filename = ("utf-8", "", filename) 

414 attachment.add_header( 

415 "Content-Disposition", "attachment", filename=filename 

416 ) 

417 return attachment 

418 

419 def _set_list_header_if_not_empty(self, msg, header, values): 

420 """ 

421 Set msg's header, either from self.extra_headers, if present, or from 

422 the values argument. 

423 """ 

424 if values: 

425 try: 

426 value = self.extra_headers[header] 

427 except KeyError: 

428 value = ", ".join(str(v) for v in values) 

429 msg[header] = value 

430 

431 

432class EmailMultiAlternatives(EmailMessage): 

433 """ 

434 A version of EmailMessage that makes it easy to send multipart/alternative 

435 messages. For example, including text and HTML versions of the text is 

436 made easier. 

437 """ 

438 

439 alternative_subtype = "alternative" 

440 

441 def __init__( 

442 self, 

443 subject="", 

444 body="", 

445 from_email=None, 

446 to=None, 

447 bcc=None, 

448 connection=None, 

449 attachments=None, 

450 headers=None, 

451 alternatives=None, 

452 cc=None, 

453 reply_to=None, 

454 ): 

455 """ 

456 Initialize a single email message (which can be sent to multiple 

457 recipients). 

458 """ 

459 super().__init__( 

460 subject, 

461 body, 

462 from_email, 

463 to, 

464 bcc, 

465 connection, 

466 attachments, 

467 headers, 

468 cc, 

469 reply_to, 

470 ) 

471 self.alternatives = alternatives or [] 

472 

473 def attach_alternative(self, content, mimetype): 

474 """Attach an alternative content representation.""" 

475 if content is None or mimetype is None: 

476 raise ValueError("Both content and mimetype must be provided.") 

477 self.alternatives.append((content, mimetype)) 

478 

479 def _create_message(self, msg): 

480 return self._create_attachments(self._create_alternatives(msg)) 

481 

482 def _create_alternatives(self, msg): 

483 encoding = self.encoding or settings.DEFAULT_CHARSET 

484 if self.alternatives: 

485 body_msg = msg 

486 msg = SafeMIMEMultipart( 

487 _subtype=self.alternative_subtype, encoding=encoding 

488 ) 

489 if self.body: 

490 msg.attach(body_msg) 

491 for alternative in self.alternatives: 

492 msg.attach(self._create_mime_attachment(*alternative)) 

493 return msg