attachment.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. from email.message import EmailMessage
  2. from email.mime.base import MIMEBase
  3. from email.mime.text import MIMEText
  4. from email.mime.multipart import MIMEMultipart
  5. from email.mime.application import MIMEApplication
  6. import io
  7. from pathlib import Path, PurePath
  8. from typing import Union
  9. from .utils import PIL, plt
  10. import pandas as pd
  11. class Attachments:
  12. def __init__(self, attachments:Union[list, dict], encoding='UTF-8'):
  13. self.attachments = attachments
  14. self.encoding = encoding
  15. def attach(self, msg:EmailMessage):
  16. for part in self._get_parts():
  17. msg.attach(part)
  18. def _get_parts(self):
  19. if isinstance(self.attachments, dict):
  20. for name, cont in self.attachments.items():
  21. yield self._get_part_named(cont, name=name)
  22. elif isinstance(self.attachments, (list, set, tuple)):
  23. for cont in self.attachments:
  24. yield self._get_part(cont)
  25. else:
  26. # A single attachment
  27. yield self._get_part(self.attachments)
  28. def _get_part(self, item) -> MIMEBase:
  29. cont = self._get_bytes(item)
  30. filename = self._get_filename(item)
  31. part = MIMEApplication(cont)
  32. part.add_header(
  33. "Content-Disposition",
  34. "attachment", filename=filename
  35. )
  36. part.add_header('Content-Transfer-Encoding', 'base64')
  37. return part
  38. def _get_part_named(self, item, name) -> MIMEBase:
  39. cont = self._get_bytes_named(item, name)
  40. part = MIMEApplication(cont)
  41. part.add_header(
  42. "Content-Disposition",
  43. "attachment", filename=name
  44. )
  45. return part
  46. def _get_bytes(self, item) -> bytes:
  47. if isinstance(item, str):
  48. # Considered as path
  49. if Path(item).is_file():
  50. return Path(item).read_bytes()
  51. else:
  52. raise ValueError(f"Unknown attachment '{item}'. Perhaps a mistyped path?")
  53. elif isinstance(item, PurePath):
  54. return item.read_bytes()
  55. elif isinstance(item, (pd.DataFrame, pd.Series)):
  56. return item.to_csv().encode(self.encoding)
  57. elif isinstance(item, (bytes, bytearray)):
  58. return item
  59. else:
  60. raise TypeError(f"Unknown attachment {type(item)}")
  61. def _get_bytes_named(self, item, name:str) -> bytes:
  62. if isinstance(item, str):
  63. # Considered as raw document
  64. return item
  65. elif isinstance(item, PurePath):
  66. return item.read_bytes()
  67. elif isinstance(item, (pd.DataFrame, pd.Series)):
  68. buff = io.BytesIO()
  69. if name.endswith(".xlsx"):
  70. item.to_excel(buff)
  71. return buff.getvalue()
  72. elif name.endswith(".csv"):
  73. return item.to_csv().encode(self.encoding)
  74. elif name.endswith(".html"):
  75. return item.to_html().encode(self.encoding)
  76. elif name.endswith('.txt'):
  77. return str(item)
  78. else:
  79. raise ValueError(f"Unknown dataframe conversion for '{name}'")
  80. elif isinstance(item, (bytes, bytearray)):
  81. return item
  82. elif PIL is not None and isinstance(item, PIL.Image.Image):
  83. buf = io.BytesIO()
  84. item.save(buf, format='PNG')
  85. buf.seek(0)
  86. return buf.read()
  87. elif plt is not None and isinstance(item, plt.Figure):
  88. buf = io.BytesIO()
  89. item.savefig(buf, format=Path(name).suffix[1:])
  90. buf.seek(0)
  91. return buf.read()
  92. else:
  93. raise TypeError(f"Unknown attachment {type(item)} ({name})")
  94. def _get_filename(self, item):
  95. if isinstance(item, str):
  96. # Considered as path
  97. if Path(item).is_file():
  98. return Path(item).name
  99. return item
  100. elif isinstance(item, PurePath):
  101. return item.name
  102. else:
  103. raise TypeError(f"Cannot figure out filename for {item}")