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