1
0

attachment.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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. # NOTE: the validation it is a file should already be
  96. # done
  97. return Path(item).name
  98. elif isinstance(item, PurePath):
  99. return item.name
  100. else: # pragma: no cover
  101. # NOTE: this piece of code should not be run as the type check is
  102. # done before. If this is run, then there is an unfinished feature.
  103. raise NotImplementedError(f"Cannot figure out filename for {item}")