Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief define an Email grabbed from a server. 

5""" 

6 

7import os 

8import re 

9import json 

10import datetime 

11import email 

12from email.generator import BytesGenerator, Generator 

13import email.header 

14import email.message 

15from io import BytesIO, StringIO 

16import mimetypes 

17import hashlib 

18import warnings 

19from collections import OrderedDict 

20import dateutil.parser 

21from pyquickhelper.loghelper import noLOG 

22 

23from .mail_exception import MailException 

24from .additional_mime_type import additional_mime_type_ext_type 

25 

26 

27class EmailMessage(email.message.Message): 

28 

29 """ 

30 overloads the message to class to add some 

31 functionalities such as a display using HTML 

32 """ 

33 

34 expMail1 = re.compile('(\\"([^;,]*?)\\" )?<([^;, ]+?@[^;, ]+)>') 

35 expMail2 = re.compile('(([^;,]*?) )?<([^;, ]+?@[^;, ]+)>') 

36 expMail3 = re.compile('(\\"([^;,]*?)\\" )?([^;, ]+?@[^;, ]+)') 

37 expMail4 = re.compile('((=[?]([^;,]+?)[?]=)? ?<([^;, ]+?@[^;, ]+)>)') 

38 expMailA = re.compile( 

39 '({0})|({1})|({2})'.format( 

40 expMail1.pattern, 

41 expMail2.pattern, 

42 expMail3.pattern)) 

43 

44 subset = ["Date", "From", "Subject", "To", "X-bcc"] 

45 avoid = ["X-me-spamcause", "X-YMail-OSG"] 

46 

47 additionnalMimeType = additional_mime_type_ext_type 

48 _date_format = "%Y-%m-%dT%H:%M:%S.%fZ" 

49 

50 def as_bytes(self): # pylint: disable=W0221 

51 """ 

52 converts the mail into a binary string 

53 

54 @return bytes 

55 

56 See `Message.as_bytes <https://docs.python.org/3/library/email.message.html#email.message.Message.as_bytes>`_ 

57 """ 

58 fp = BytesIO() 

59 g = BytesGenerator(fp, mangle_from_=True, maxheaderlen=60) 

60 g.flatten(self) 

61 return fp.getvalue() 

62 

63 def as_string(self, unixfrom=False, maxheaderlen=None, policy=None): 

64 """ 

65 Converts the mail into a string. 

66 

67 @return string 

68 

69 See `Message.as_string <https://docs.python.org/3/library/email.message.html#email.message.Message.as_string>`_ 

70 """ 

71 fp = StringIO() 

72 g = Generator(fp, mangle_from_=True, maxheaderlen=60) 

73 g.flatten(self) 

74 return fp.getvalue() 

75 

76 @staticmethod 

77 def create_from_bytes(b): 

78 """ 

79 Creates an instance of @see cl EmailMessage 

80 from a binary string (bytes) (see @see me as_bytes). 

81 

82 @param b binary string 

83 @return instance of @see cl EmailMessage 

84 """ 

85 return email.message_from_bytes(b, _class=EmailMessage) 

86 

87 @property 

88 def body(self): 

89 """ 

90 return the body of the message 

91 """ 

92 messages = [] 

93 for part in self.walk(): 

94 if part.get_content_type() == "text/html": 

95 b = part.get_payload(decode=1) 

96 if b is not None: 

97 encs = [part.get_content_charset(), "utf8"] 

98 s = None 

99 for enc in encs: 

100 try: 

101 s = b.decode(enc) 

102 except UnicodeDecodeError: 

103 continue 

104 if s is None: 

105 raise UnicodeDecodeError( 

106 "unable to decode: {0}".format(b)) 

107 messages.append(s) 

108 return "\n------------------------------------------\n\n".join( 

109 messages) 

110 

111 def get_all_charsets(self, part=None): 

112 """ 

113 returns all the charsets 

114 """ 

115 if part is None: 

116 charsets = set({}) 

117 for c in self.get_charsets(): 

118 if c is not None: 

119 charsets.update([c]) 

120 return charsets 

121 else: 

122 charsets = set({}) 

123 for c in part.get_charsets(): 

124 if c is not None: 

125 charsets.update([c]) 

126 return charsets 

127 

128 def get_nb_attachements(self): 

129 """ 

130 return the number of attachments 

131 

132 @return int 

133 """ 

134 r = 0 

135 for part in self.walk(): 

136 if part.get_content_maintype() == 'multipart': 

137 continue 

138 if part.get('Content-Disposition') is None: 

139 continue 

140 r += 1 

141 return r 

142 

143 @property 

144 def body_html(self): 

145 """ 

146 return the body of the messag 

147 """ 

148 messages = [] 

149 for part in self.walk(): 

150 if part.get_content_type() == "text/html": 

151 b = part.get_payload(decode=1) 

152 if b is not None: 

153 chs = list(self.get_all_charsets(part)) 

154 if len(chs) > 0: 

155 try: 

156 ht = b.decode(chs[0]) 

157 except UnicodeDecodeError: 

158 try: 

159 ht = b.decode("utf-8") 

160 except UnicodeDecodeError: 

161 try: 

162 ht = b.decode("latin-1") 

163 except UnicodeDecodeError: 

164 raise Exception( # pylint: disable=W0707 

165 "unable to decode %r: %r" % (chs[0], b)) 

166 else: 

167 try: 

168 ht = b.decode("utf-8") 

169 except UnicodeDecodeError: 

170 ht = b.decode("utf-8", errors='ignore') 

171 #raise MailException("unable to decode: " + str(b)) from e 

172 htl = ht.lower() 

173 pos = htl.find("<body") 

174 pos2 = htl.find("</body>") 

175 if pos != -1 and pos2 != -1: 

176 ht = '<div ' + ht[pos + 5:pos2] + "</div>" 

177 elif pos != -1: 

178 ht = '<div ' + ht[pos + 5:] + "</div>" 

179 elif pos2 != -1: 

180 ht = '<div>' + ht[:pos2] + "</div>" 

181 else: 

182 ht = '<div>' + ht + "</div>" 

183 messages.append(ht) 

184 text = "<hr />".join(messages) 

185 return text 

186 

187 def enumerate_attachments(self): 

188 """ 

189 enumerate the attachments as 

190 4-uple (filename, content, message_id, content_id) 

191 

192 @return iterator on tuple (filename, content, message_id, content_id) 

193 """ 

194 for part in self.walk(): 

195 if part.get_content_maintype() == 'multipart': 

196 continue 

197 if part.get('Content-Disposition') is None: 

198 continue 

199 

200 fileName = part.get_filename() 

201 fileName = self.decode_header("file", fileName) 

202 

203 if fileName is not None and fileName.startswith( 

204 "=?") and fileName.startswith("?="): 

205 fileName = fileName.strip("=?").split("=")[-1] # pylint: disable=C0207 

206 

207 if fileName is None or "?" in fileName: 

208 fileName = "unknown_type" 

209 cont = part.get_payload(decode=True) 

210 cont_id = part["Message-ID"] 

211 cont_id2 = part["Content-ID"] 

212 ext = EmailMessage.additionnalMimeType.get( 

213 part.get_content_subtype(), 

214 None) 

215 if ext is None: 

216 ext = mimetypes.guess_extension(part.get_content_type()) 

217 

218 if ext is not None: 

219 fileName += ext 

220 elif cont is not None: 

221 if cont.startswith(b"%PDF"): 

222 fileName += ".pdf" 

223 elif part.get_content_maintype() == "text": 

224 if cont.startswith(b"<html>"): 

225 fileName += ".html" 

226 else: 

227 fileName += ".txt" 

228 else: 

229 raise MailException("unable to guess type: " + 

230 part.get_content_maintype() + 

231 "\nsubtype: " + 

232 str(part.get_content_subtype()) + 

233 " ext: " + 

234 str(ext) + 

235 " def: " + 

236 EmailMessage.additionnalMimeType.get(part.get_content_subtype(), "-") + 

237 "\n" + 

238 str([cont])) 

239 else: 

240 cont = part.get_payload(decode=True) 

241 cont_id = part[ 

242 "Message-ID"].strip("<>") if part["Message-ID"] else None 

243 cont_id2 = part[ 

244 "Content-ID"].strip("<>") if part["Content-ID"] else None 

245 

246 yield fileName, cont, cont_id, cont_id2 

247 

248 def __sortkey__(self): 

249 """ 

250 usual 

251 """ 

252 key = [self.get_date(), self.get_from(), self.get_to(), 

253 self.UniqueID, self["subject"]] 

254 for i, k in enumerate(key): 

255 if isinstance(k, tuple): 

256 if None in k: 

257 key[i] = tuple("" if _ is None else _ for _ in k) 

258 elif k is None: 

259 key[i] = "" 

260 return tuple(key) 

261 

262 def __lt__(self, at): 

263 """ 

264 usual 

265 """ 

266 try: 

267 return self.__sortkey__() < at.__sortkey__() 

268 except TypeError as e: 

269 raise Exception("issue with\n{0}\n{1}".format( 

270 self.__sortkey__(), at.__sortkey__())) from e 

271 

272 #: use for method @see me call_decode_header 

273 _search_encodings = ["iso-8859-1", "windows-1252", "UTF-8", "utf-8"] 

274 

275 @staticmethod 

276 def call_decode_header(st, is_email=False): 

277 """ 

278 call `email.header.decode_header <https://docs.python.org/3.4/library/email.header.html#email.header.decode_header>`_ 

279 

280 @param st string or `email.header.Header <https://docs.python.org/3.4/library/email.header.html#email.header.Header>`_ 

281 @param is_email does something specific for emails 

282 @return text, encoding 

283 """ 

284 if isinstance(st, email.header.Header): 

285 text, encoding = email.header.decode_header(st)[0] 

286 if isinstance(text, bytes): 

287 if encoding is None: 

288 raise ValueError( 

289 "encoding cannot be None if the returned string is bytes") 

290 if encoding == "unknown-8bit": 

291 try: 

292 res = text.decode("utf8") 

293 except UnicodeDecodeError: 

294 res = text.decode("ascii", errors="ignore") 

295 # raise ValueError("encoding {0} is unexpected in\n{1}".format(encoding, st)) from e 

296 return res, "ascii" 

297 else: 

298 return text.decode(encoding), encoding 

299 else: 

300 return text, encoding 

301 elif isinstance(st, str): 

302 if is_email: 

303 zall = EmailMessage.expMail4.findall(st) 

304 if zall: 

305 res = [] 

306 for add in zall: 

307 head, enc = EmailMessage.call_decode_header( 

308 add[0], is_email=False) 

309 fin = "{0} <{1}>".format(head, add[-1]) 

310 res.append(fin) 

311 return "; ".join(res), enc 

312 else: 

313 return EmailMessage.call_decode_header(st, is_email=False) 

314 else: 

315 text, encoding = email.header.decode_header(st)[0] 

316 if isinstance(text, bytes): 

317 position = None 

318 if encoding is None: 

319 # maybe the string contrains several encoding 

320 for enc in EmailMessage._search_encodings: 

321 look = "=?%s?" % enc 

322 if look in st: 

323 position = st.find(look) 

324 

325 if position == 0: 

326 # otherwise we face an infinite loop 

327 position = None 

328 

329 if position is not None: 

330 first = st[:position] 

331 second = st[position:] 

332 dec1, enc1 = EmailMessage.call_decode_header(first) 

333 dec2, enc2 = EmailMessage.call_decode_header( 

334 second) 

335 

336 if isinstance(dec1, str) and isinstance(dec2, str): 

337 enc = enc2 if enc1 is None else enc1 

338 return dec1 + dec2, enc 

339 else: 

340 mes = ('decoding issue\n File "{0}", line {1},\nunable to decode ' + 

341 'string:\n{2}\neven split into:\n1: {3}\n2: {4}') 

342 warnings.warn(mes.format(__file__, 250, st.replace("\r", " ").replace("\n", " "), 

343 first.replace("\r", " ").replace( 

344 "\n", " "), 

345 second.replace("\r", " ").replace("\n", " "))) 

346 return st, None 

347 else: 

348 warnings.warn( 

349 'decoding issue\n File "{0}", line {1},\nunable to decode string:\n{2}'.format( 

350 __file__, 

351 260, 

352 st.replace( 

353 "\r", 

354 " ").replace( 

355 "\n", 

356 " "))) 

357 return st, None 

358 else: 

359 return text.decode(encoding), encoding 

360 else: 

361 return text, encoding 

362 else: 

363 raise TypeError("cannot decode type: {0}".format(type(st))) 

364 

365 def get_from_str(self): 

366 """ 

367 return a string for the receivers 

368 

369 @return string 

370 """ 

371 l, a = self.get_from() 

372 res = [] 

373 if l: 

374 res.append(l) 

375 else: 

376 res.append(a) 

377 return ";".join(res) 

378 

379 def get_from(self): 

380 """ 

381 returns a tuple (label, email address) or a list of groups 

382 from the regular expression 

383 

384 @return tuple ( label, email address) 

385 """ 

386 st = self["from"] 

387 if isinstance(st, email.header.Header): 

388 text, _ = EmailMessage.call_decode_header(st, is_email=True) 

389 if text is None: 

390 raise MailException( 

391 "unable to parse: " + 

392 str(text) + 

393 "\n" + 

394 str(st)) 

395 else: 

396 text = st 

397 

398 cp = EmailMessage.expMail1.search(text) 

399 if not cp: 

400 cp = EmailMessage.expMail2.search(text) 

401 if not cp: 

402 cp = EmailMessage.expMail3.search(text) 

403 if not cp: 

404 if text.startswith('"=?utf-8?'): 

405 text = text.strip('"') 

406 text, _ = EmailMessage.call_decode_header( 

407 text, is_email=True) 

408 gr = cp.groups() 

409 name, mail = gr[1], gr[2] 

410 if name is None: 

411 name = self.get_name(_fallback_get_from=False) 

412 elif name.startswith("=?"): 

413 name = EmailMessage.call_decode_header(name)[0] 

414 if name is None: 

415 name = gr[1] 

416 return name, mail 

417 

418 def get_name(self, _fallback_get_from=True): 

419 """ 

420 return the sender name of an email (if available) 

421 

422 @param _fallback_get_from internal parameter, avoir recursion 

423 @return name (or None if not found) 

424 """ 

425 st = self["from"] 

426 if isinstance(st, email.header.Header): 

427 text, _ = EmailMessage.call_decode_header(st, is_email=True) 

428 if text is None: 

429 raise MailException( 

430 "unable to parse: " + 

431 str(text) + 

432 "\n" + 

433 str(st)) 

434 elif st.startswith("=?"): 

435 text, _ = EmailMessage.call_decode_header(st) 

436 else: 

437 text = st 

438 

439 if "<" in text: 

440 r = text.split("<")[0].strip() 

441 return r if r else None 

442 elif text is None and _fallback_get_from: 

443 return self.get_from()[0] 

444 else: 

445 return text 

446 

447 def get_to_str(self, cc=False, field="to"): 

448 """ 

449 return a string for the receivers 

450 

451 @param cc get receivers or second receivers 

452 @param field field to use, ``to`` or ``Delivered-To`` 

453 (the second one is used as a backup anyway) 

454 @return string 

455 """ 

456 to = self.get_to(cc=cc, field=field) 

457 if to is None: 

458 return "" 

459 res = [] 

460 for li, a in to: # pylint: disable=E1133 

461 if li: 

462 res.append(li) 

463 else: 

464 res.append(a) 

465 return ";".join(res) 

466 

467 def get_to(self, cc=False, field="to"): 

468 """ 

469 return the receivers 

470 

471 @param cc get receivers or second receivers 

472 @param field field to use, ``to`` or ``Delivered-To`` 

473 (the second one is used as a backup anyway) 

474 @return list of tuple [ ( label, email address) ] 

475 """ 

476 st = self[field if not cc else "cc"] 

477 if st is None and not cc: 

478 st = self["Delivered-To"] 

479 if st is None: 

480 return None 

481 text, _ = EmailMessage.call_decode_header(st, is_email=True) 

482 if text is None: 

483 raise MailException("unable to parse: " + str(st)) 

484 

485 def find_unnone(ens): 

486 "local function" 

487 for c in ens: 

488 if c is not None: 

489 return c 

490 return None 

491 

492 text = text.replace("\r", " ").replace("\n", " ").replace("\t", " ") 

493 cp = [] 

494 for st in EmailMessage.expMailA.finditer(text): 

495 gr = st.groups() 

496 if len(gr) != 12: 

497 raise MailException( 

498 "unexpected error due to a change in regular expressions") 

499 values = gr[2], gr[3], gr[6], gr[7], gr[10], gr[11] 

500 label = find_unnone(values[::2]) 

501 add = find_unnone(values[1::2]) 

502 if label is not None: 

503 label = label.strip(" \r\n\t") 

504 text, _ = EmailMessage.call_decode_header( 

505 label, is_email=True) 

506 if text.startswith('"=?utf-8?'): 

507 text = text.strip('"') 

508 text = EmailMessage.call_decode_header( 

509 text, is_email=True)[0] 

510 cp.append((text, add)) 

511 else: 

512 cp.append((None, add)) 

513 

514 return cp if cp else None 

515 

516 def get_date(self): 

517 """ 

518 return a datetime object for the field Date 

519 """ 

520 st = self["Date"] 

521 res, _ = EmailMessage.call_decode_header(st) 

522 if res is None: 

523 raise MailException("unable to parse: " + str(st)) 

524 

525 try: 

526 p = dateutil.parser.parse(res) 

527 except Exception as e: 

528 # it can fail because of dates such as: Wed, 7 Oct 2009 11:43:56 

529 # +0200 (Paris, Madrid (heure d'\ufffdt\ufffd)) 

530 if "(" in res: 

531 res = res[:res.find("(")] 

532 p = dateutil.parser.parse(res) 

533 return p 

534 else: 

535 if "," in res: 

536 b = res.split(",")[1] 

537 try: 

538 p = dateutil.parser.parse(b) 

539 except Exception as e: 

540 raise MailException( 

541 "unable to parse: " + 

542 str(res) + 

543 "\n" + 

544 str(st)) from e 

545 else: 

546 raise MailException( 

547 "unable to parse: " + 

548 str(res) + 

549 "\n" + 

550 str(st)) from e 

551 if p is None: 

552 raise MailException( 

553 "unable to parse: " + 

554 str(res) + 

555 "\n" + 

556 str(st)) 

557 return p 

558 

559 def get_date_str(self): 

560 """ 

561 return the date into a string 

562 

563 @return date as a string (iso format) 

564 """ 

565 return self.get_date().strftime(EmailMessage._date_format) 

566 

567 def default_filename(self): 

568 """ 

569 define a default filename (no extension) 

570 

571 @return str 

572 """ 

573 b = self.get_from()[1] 

574 if len(b) == 0: 

575 raise MailException("from is unknown: " + self["from"]) 

576 b = b.replace("@", "-at-").replace(".", "-") 

577 date = self.get_date() 

578 d = "%04d-%02d-%02d" % (date.year, date.month, date.day) 

579 f = "d_{0}_p_{1}_ii_{2}".format(d, b, self.UniqueID) 

580 return f.replace( 

581 "\\", "-").replace("\r", "").replace("\n", "-").replace("%", "-").replace("/", "-") 

582 

583 @staticmethod 

584 def interpret_default_filename(name): 

585 """ 

586 reverse engineer method @see me default_filename 

587 

588 @param name filename 

589 @return dictionary 

590 

591 The function creates a dictionary with keys date, from, uid, name. 

592 """ 

593 pieces = name.split("_") 

594 res = {} 

595 for i, p in enumerate(pieces): 

596 if p == "d" and "date" not in res: 

597 res["date"] = pieces[i + 1] 

598 elif p == "p" and "from" not in res: 

599 res["from"] = pieces[i + 1] 

600 elif p == "ii" and "uid" not in res: 

601 res["uid"] = pieces[i + 1].split(".")[0] 

602 res["name"] = name 

603 return res 

604 

605 @property 

606 def UniqueID(self): 

607 """ 

608 builds a unique ID 

609 """ 

610 md5 = hashlib.md5() 

611 t = self["Message-ID"] 

612 if t is not None: 

613 md5.update(t.encode('utf-8')) 

614 else: 

615 for f in ["Subject", "To", "From", "Date"]: 

616 if self[f] is not None: 

617 md5.update(self[f].encode('utf-8')) 

618 return md5.hexdigest() 

619 

620 def decode_header(self, field, st): 

621 """ 

622 decode a string encoded in the header 

623 

624 @param field field 

625 @param st string 

626 @return string (it never return None) 

627 """ 

628 if st is None: 

629 return "" 

630 elif isinstance(st, str): 

631 if st.startswith("Tr:") and field.lower() == "subject": 

632 pos = st.find("=?") 

633 return st[:pos] + self.decode_header(field, st[pos:]) 

634 else: 

635 text, _ = EmailMessage.call_decode_header(st) 

636 return text if text is not None else "" 

637 elif isinstance(st, bytes): 

638 text, _ = EmailMessage.call_decode_header(st) 

639 return self.decode_header(field, text) if text is not None else "" 

640 elif isinstance(st, email.header.Header): 

641 text = EmailMessage.call_decode_header(st)[0] 

642 return self.decode_header(field, text) if text is not None else "" 

643 else: 

644 raise MailException( 

645 "unable to process type " + str(type(st)) + "\n" + str(st)) 

646 

647 def get_field(self, field): 

648 """ 

649 get a field and cleans it 

650 

651 @param field subject or ... 

652 @return text 

653 """ 

654 subj = self[field] 

655 if subj is None: 

656 subj = self[field] 

657 if subj is not None: 

658 subj = self.decode_header(field, subj) 

659 return subj 

660 

661 @property 

662 def Fields(self): 

663 """ 

664 @return list of available fields 

665 """ 

666 return list(sorted(self.keys())) 

667 

668 def to_dict(self): 

669 """ 

670 Returns all fields for an emails as a dictionary 

671 @return dictionary { key : value } 

672 """ 

673 res = OrderedDict((k, self.get_field(k)) for k in self.Fields) 

674 res["attached"] = self.get_nb_attachements() 

675 return res 

676 

677 def dump_attachments(self, attach_folder=".", buffer_write=None, metadata=True, fLOG=noLOG): 

678 """ 

679 Dumps the mail into a folder using HTML format. 

680 If the destination files already exists, it skips it. 

681 If an attachments already has the same name, it chooses another one if 

682 the attachment is different (otherwise it keeps it as it is). 

683 

684 @param attach_folder destination folder 

685 @param buffer_write None or instance of @see cl BufferFilesWriting 

686 @param metadata if True, also dump metadata about attachments 

687 @param fLOG logging function 

688 @return list of attachments 

689 

690 The results is a list of 3-uple: 

691 

692 * full name of the attachments 

693 * message id 

694 * content id 

695 

696 The metadata contains information about the mail it comes from. 

697 The data is stored in a json format (except for date). 

698 It is stored in a file with extension ``.metadata``. 

699 """ 

700 def local_exists(name): 

701 "local function" 

702 if buffer_write: 

703 return buffer_write.exists(name) 

704 else: 

705 return os.path.exists(name) 

706 

707 def local_different(to, content): 

708 "local function" 

709 if buffer_write: 

710 c2 = buffer_write.read_binary_content(to) 

711 else: 

712 with open(to, "rb") as f: 

713 c2 = f.read() 

714 return c2 != content 

715 

716 atts = [] 

717 for ai, att in enumerate(self.enumerate_attachments()): 

718 if att[1] is None: 

719 continue 

720 att_id = att[2] 

721 cont_id = att[3] 

722 to = os.path.split(att[0].replace(":", "_"))[-1] 

723 if to == '': 

724 to = 'empty_name' 

725 to = os.path.join(attach_folder, to) 

726 

727 to = to.replace("\n", "_").replace("\r", "") 

728 to = os.path.abspath(to) 

729 spl = os.path.splitext(to) 

730 

731 if "?" in to: 

732 raise MailException( 

733 "issue with attachments (mail to {0})\n{1}".format(to, att)) 

734 

735 if local_exists(to): 

736 already = True 

737 # must be different otherwise we don't do anything 

738 different = local_different(to, att[1]) 

739 if different: 

740 i = 1 

741 while local_exists(to): 

742 to = spl[0] + (".(%d)" % i) + spl[1] 

743 i += 1 

744 else: 

745 already = False 

746 different = True 

747 

748 fLOG("[dump_attachments] attachment:", to, 

749 "different={0} notnew={1}".format(different, already)) 

750 

751 if different: 

752 if metadata: 

753 d2 = dict(index=ai, filename=os.path.split(to)[-1], 

754 mail=self.default_filename() + ".html", 

755 from_=self.get_from(), to=self.get_to(), 

756 date=self.get_date_str(), uid=self.UniqueID) 

757 d2 = OrderedDict(sorted(d2.items())) 

758 st = StringIO() 

759 json.dump(d2, st) 

760 meta_text = st.getvalue() 

761 meta_name = to + ".metadata" 

762 

763 if buffer_write is None: 

764 with open(to, "wb") as f: 

765 f.write(att[1]) 

766 if metadata: 

767 with open(meta_name, "r", encoding="utf8") as f: 

768 f.write(meta_text) 

769 else: 

770 f = buffer_write.open(to, text=False) 

771 f.write(att[1]) 

772 if metadata: 

773 f = buffer_write.open(meta_name, text=True) 

774 f.write(meta_text) 

775 

776 atts.append((to, att_id, cont_id)) 

777 return atts 

778 

779 def dump(self, render, location, attach_folder="attachments", fLOG=noLOG, **params): 

780 """ 

781 Dumps a message using a call such as @see cl EmailMessageRenderer. 

782 

783 @param render instance of class @see cl EmailMessageRenderer 

784 @param location location of the file to store 

785 @param attach_folder folder for the attachments, it will be created if it does not exist 

786 @param buffer_write None or instance of @see cl BufferFilesWriting 

787 @param fLOG logging function 

788 @param params others parameters, see 

789 :meth:`EmailMessageRenderer.write <pymmails.grabber.email_message_renderer.EmailMessageRenderer.write>` 

790 @return list of stored files 

791 """ 

792 full_fold = os.path.join(location, attach_folder) 

793 atts = self.dump_attachments(full_fold, 

794 buffer_write=render.BufferWrite, 

795 fLOG=fLOG) 

796 return render.write(location=location, mail=self, 

797 filename=params.get( 

798 "filename", self.default_filename() + ".html"), 

799 attachments=atts, 

800 overwrite=params.get("overwrite", False), 

801 file_css=params.get("file_css", "mail_style.css"), 

802 encoding=params.get("encoding", "utf8"), 

803 prev_mail=params.get("prev_mail", None), 

804 next_mail=params.get("next_mail", None)) 

805 

806 @staticmethod 

807 def read_metadata(metafile): 

808 """ 

809 read metadata assuming metafile contaings a json string 

810 

811 @param metafile json string 

812 @return dictionary 

813 """ 

814 if isinstance(metafile, str): 

815 if len(metafile) < 5000 and os.path.exists(metafile): 

816 with open(metafile, "r", encoding="utf8") as f: 

817 d2 = json.load(f) 

818 else: 

819 f = StringIO(metafile) 

820 d2 = json.load(f) 

821 else: 

822 d2 = json.load(metafile) 

823 d2["date"] = datetime.datetime.strptime( 

824 d2["date"], EmailMessage._date_format) 

825 d2 = OrderedDict(sorted(d2.items())) 

826 return d2