summaryrefslogtreecommitdiffstats
path: root/core/src/compose.rs
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/compose.rs')
-rw-r--r--core/src/compose.rs430
1 files changed, 430 insertions, 0 deletions
diff --git a/core/src/compose.rs b/core/src/compose.rs
new file mode 100644
index 0000000..a928628
--- /dev/null
+++ b/core/src/compose.rs
@@ -0,0 +1,430 @@
+//! This module provides utilities for composing multipart mails.
+//!
+//! While the `Mail` type on itself can represent any multipart
+//! mail most mails have a certain pattern to their structure,
+//! consisting mainly of `multipart/mixed` for attachments,
+//! `multipart/alternative` for alternative bodies and
+//! `multipart/related` for including embedded resources which
+//! can be used in the mail bodies like e.g. a logo.
+//!
+//! This module provides the needed utilities to more simply
+//! create a `Mail` instance which represents this kind of
+//! mails.
+
+//-------------------------------------------------------------\\
+// NOTE: Implementations for creating (composing) mails are ||
+// split from the type dev, and normal impl blocks and placed ||
+// in the later part of the file for better readability. ||
+//-------------------------------------------------------------//
+
+use media_type::{MULTIPART, ALTERNATIVE, RELATED, MIXED};
+use vec1::Vec1;
+
+#[cfg(feature="serde")]
+use serde::{Serialize, Deserialize};
+
+use headers::{
+ HeaderKind,
+ headers,
+ header_components::{
+ ContentId,
+ Disposition,
+ DispositionKind,
+ MediaType
+ }
+};
+
+use ::mail::Mail;
+use ::context::Context;
+use ::resource::Resource;
+
+
+/// Parts used to create a mail body (in a multipart mail).
+///
+/// This type contains a `Resource` which is normally used
+/// to create a alternative body in a `multipart/alternative`
+/// section. As well as a number of "embeddings" which depending
+/// on there disposition can are either used as attachments
+/// or embedded resource to which the body can referre to
+/// by it's content id.
+#[derive(Debug)]
+pub struct BodyPart {
+
+ /// A body created by a template.
+ pub resource: Resource,
+
+ //TODO split in inline_embeddings, attachments ->
+ /// Embeddings added by the template engine.
+ ///
+ /// It is a mapping of the name under which a embedding had been made available in the
+ /// template engine to the embedding (which has to contain a CId, as it already
+ /// was used in the template engine and CIds are used to link to the content which should
+ /// be embedded)
+ pub embeddings: Vec<Embedded>,
+
+}
+
+/// Parts which can be used to compose a multipart mail.
+///
+/// This can be used to crate a mail, possible having
+/// attachments with multiple alternative bodies having
+/// embedded resources which can be referred to by the
+/// bodies with content ids. This embeddings can be both
+/// body specific or shared between bodies.
+///
+/// # Limitations
+///
+/// Any non alternative body will be either an attachment
+/// or an body with a inline disposition header in a
+/// `multipart/related` body. Which means you can not
+/// use this mechanism to e.g. create a `multipart/mixed`
+/// body with multiple disposition inline sub-bodies
+/// which should be displayed side-by-side. Generally this
+/// is not a very good way to create a mail, through a
+/// valid way nevertheless.
+///
+pub struct MailParts {
+ /// A vector of alternative bodies
+ ///
+ /// A typical setup would be to have two alternative bodies one text/html and
+ /// another text/plain as fallback (for which the text/plain body would be
+ /// the first in the vec and the text/html body the last one).
+ ///
+ /// Note that the order in the vector /// a additional text/plainis
+ /// the same as the order in which they will appear in the mail. I.e.
+ /// the first one is the last fallback while the last one should be
+ /// shown if possible.
+ pub alternative_bodies: Vec1<BodyPart>,
+
+ //TODO split in to vec inline_embeddings, attachments
+ /// A number of embeddings.
+ ///
+ /// Depending on the disposition of the embeddings they will be either
+ /// used as attachments or as embedded resources to which bodies can
+ /// refer by there content id. In difference to the `embeddings` field
+ /// in `BodyParts` embedded resources placed here can be used in all
+ /// bodies created by `alternative_bodies`.
+ pub embeddings: Vec<Embedded>
+}
+
+
+/// A resource embedded in a mail.
+///
+/// Depending on the deposition this will either be used
+/// to create a attachment or a embedded resources other
+/// resources can refer to by the resources content id.
+///
+#[derive(Debug, Clone)]
+#[cfg_attr(feature="serde", derive(Serialize, Deserialize))]
+pub struct Embedded {
+ content_id: Option<ContentId>,
+ resource: Resource,
+ disposition: DispositionKind,
+}
+
+impl Embedded {
+
+ /// Create a inline embedding from an `Resource`.
+ pub fn inline(resource: Resource) -> Self {
+ Embedded::new(resource, DispositionKind::Inline)
+ }
+
+ /// Create a attachment embedding from an `Resource`.
+ pub fn attachment(resource: Resource) -> Self {
+ Embedded::new(resource, DispositionKind::Attachment)
+ }
+
+ /// Create a new embedding from a resource using given disposition.
+ pub fn new(resource: Resource, disposition: DispositionKind) -> Self {
+ Embedded {
+ content_id: None,
+ resource,
+ disposition
+ }
+ }
+
+ /// Create a new embedding from a `Resource` using given disposition and given content id.
+ pub fn with_content_id(resource: Resource, disposition: DispositionKind, content_id: ContentId) -> Self {
+ Embedded {
+ content_id: Some(content_id),
+ resource,
+ disposition
+ }
+ }
+
+ /// Return a reference to the contained resource.
+ pub fn resource(&self) -> &Resource {
+ &self.resource
+ }
+
+ /// Return a mutable reference to the contained resource.
+ pub fn resource_mut(&mut self) -> &mut Resource {
+ &mut self.resource
+ }
+
+ /// Return a reference to the contained content id, if any.
+ pub fn content_id(&self) -> Option<&ContentId> {
+ self.content_id.as_ref()
+ }
+
+ /// Return a reference to disposition to use for the embedding.
+ pub fn disposition(&self) -> DispositionKind {
+ self.disposition
+ }
+
+ /// Generate and set a new content id if this embedding doesn't have a content id.
+ pub fn assure_content_id(&mut self, ctx: &impl Context) -> &ContentId {
+ if self.content_id.is_none() {
+ self.content_id = Some(ctx.generate_content_id());
+ }
+
+ self.content_id().unwrap()
+ }
+}
+
+
+//-------------------------------------------------------\\
+// implementations for creating mails are from here on ||
+//-------------------------------------------------------//
+
+
+impl MailParts {
+
+ /// Generating content ids for all contained `Embedded` instances which don't have a cid.
+ ///
+ pub fn generate_content_ids(&mut self, ctx: &impl Context) {
+ for body in self.alternative_bodies.iter_mut() {
+ for embedding in body.embeddings.iter_mut() {
+ embedding.assure_content_id(ctx);
+ }
+ }
+
+ for embedding in self.embeddings.iter_mut() {
+ embedding.assure_content_id(ctx);
+ }
+ }
+
+
+ /// Create a `Mail` instance based on this `MailParts` instance.
+ ///
+ /// This will first generate content ids for all contained
+ /// `Embedded` instances.
+ ///
+ /// If this instance contains any attachments then the
+ /// returned mail will be a `multipart/mixed` mail with
+ /// the first body containing the actual mail and the
+ /// other bodies containing the attachments.
+ ///
+ /// If the `MailParts.embeddins` is not empty then the
+ /// mail will be wrapped in `multipart/related` (inside
+ /// any potential `multipart/mixed`) containing hte
+ /// actual mail in the first body and the embeddings
+ /// in the other bodies.
+ ///
+ /// The mail will have a `multipart/alternative` body
+ /// if it has more then one alternative body
+ /// (inside a potential `multipart/related` inside a
+ /// potential `multipart/mixed` body). This body contains
+ /// one sub-body for each `BodyPart` instance in
+ /// `MailParts.alternative_bodies`.
+ ///
+ /// Each sub-body created for a `BodyPart` will be wrapped
+ /// inside a `multipart/related` if it has body specific
+ /// embeddings (with content disposition inline).
+ pub fn compose_mail(mut self, ctx: &impl Context)
+ -> Mail
+ {
+ self.generate_content_ids(ctx);
+ self.compose_without_generating_content_ids()
+ }
+
+ /// This function works like `compose_mail` but does not generate
+ /// any content ids.
+ pub fn compose_without_generating_content_ids(self)
+ -> Mail
+ {
+ let MailParts { alternative_bodies, embeddings } = self;
+
+ let mut attachments = Vec::new();
+ let mut alternatives = alternative_bodies.into_iter()
+ .map(|body| body.create_mail(&mut attachments))
+ .collect::<Vec<_>>();
+
+ let embeddings = embeddings.into_iter()
+ .filter_map(|emb| {
+ let disposition = emb.disposition();
+ let mail = emb.create_mail();
+ if disposition == DispositionKind::Attachment {
+ attachments.push(mail);
+ None
+ } else {
+ Some(mail)
+ }
+ })
+ .collect::<Vec<_>>();
+
+ //UNWRAP_SAFE: bodies is Vec1, i.e. we have at last one
+ let mail = alternatives.pop().unwrap();
+ let mail =
+ if alternatives.is_empty() {
+ mail
+ } else {
+ mail.wrap_with_alternatives(alternatives)
+ };
+
+ let mail =
+ if embeddings.is_empty() {
+ mail
+ } else {
+ mail.wrap_with_related(embeddings)
+ };
+
+ let mail =
+ if attachments.is_empty() {
+ mail
+ } else {
+ mail.wrap_with_mixed(attachments)
+ };
+
+ mail
+ }
+}
+
+impl BodyPart {
+
+ /// Creates a `Mail` instance from this `BodyPart` instance.
+ ///
+ /// All embeddings in `BodyPart.embeddings` which have a
+ /// attachment content disposition are placed into the
+ /// `attachments_out` parameter, as attachments should
+ /// always be handled on the outer most level but the
+ /// produced mail is likely not the outer most level.
+ ///
+ /// This will create a non-multipart body for the
+ /// body `Resource`, if there are any embeddings which
+ /// have a `Inline` disposition that body will be
+ /// wrapped into a `multipart/related` body containing
+ /// them.
+ pub fn create_mail(self, attachments_out: &mut Vec<Mail>)
+ -> Mail
+ {
+ let BodyPart { resource, embeddings } = self;
+ let body = resource.create_mail();
+
+ let related = embeddings.into_iter()
+ .filter_map(|embedded| {
+ let disposition = embedded.disposition();
+ let emb_mail = embedded.create_mail();
+ if disposition == DispositionKind::Attachment {
+ attachments_out.push(emb_mail);
+ None
+ } else {
+ Some(emb_mail)
+ }
+ })
+ .collect::<Vec<_>>();
+
+ if related.is_empty() {
+ body
+ } else {
+ body.wrap_with_related(related)
+ }
+ }
+}
+
+impl Embedded {
+
+ /// Create a `Mail` instance for this `Embedded` instance.
+ ///
+ /// This will create a non-multipart mail based on the contained
+ /// resource containing a `Content-Disposition` header as well as an
+ /// `Content-Id` header if it has a content id.
+ ///
+ pub fn create_mail(self) -> Mail {
+ let Embedded {
+ content_id,
+ resource,
+ disposition:disposition_kind
+ } = self;
+
+ let mut mail = resource.create_mail();
+ if let Some(content_id) = content_id {
+ mail.insert_header(headers::ContentId::body(content_id));
+ }
+ let disposition = Disposition::new(disposition_kind, Default::default());
+ mail.insert_header(headers::ContentDisposition::body(disposition));
+ mail
+ }
+}
+
+impl Resource {
+
+ /// Create a `Mail` instance representing this `Resource`.
+ ///
+ /// This is not a complete mail, i.e. it will not contain
+ /// headers like `From` or `To` and in many cases the
+ /// returned `Mail` instance will be wrapped into other
+ /// mail instances adding alternative bodies, embedded
+ /// resources and attachments.
+ pub fn create_mail(self) -> Mail {
+ Mail::new_singlepart_mail(self)
+ }
+}
+
+impl Mail {
+
+ /// Create a `multipart/mixed` `Mail` instance containing this mail as
+ /// first body and one additional body for each attachment.
+ ///
+ /// Normally this is used with embeddings having a attachment
+ /// disposition creating a mail with attachments.
+ pub fn wrap_with_mixed(self, other_bodies: Vec<Mail>)
+ -> Mail
+ {
+ let mut bodies = other_bodies;
+ bodies.push(self);
+ new_multipart(&MIXED, bodies)
+ }
+
+ /// Create a `multipart/alternative` `Mail` instance containing this
+ /// mail as the _main_ body with given alternatives.
+ ///
+ /// The "priority" of alternative bodies is ascending with the body
+ /// which should be shown only if all other bodies can't be displayed
+ /// first. I.e. the order is the same order as
+ /// specified by `multipart/alternative`.
+ /// This also means that _this_ body will be the last body as it is
+ /// meant to be the _main_ body.
+ pub fn wrap_with_alternatives(self, alternates: Vec<Mail>)
+ -> Mail
+ {
+ let mut bodies = alternates;
+ bodies.insert(0, self);
+ new_multipart(&ALTERNATIVE, bodies)
+ }
+
+ /// Creates a `multipart/related` `Mail` instance containing this
+ /// mail first and then all related bodies.
+ pub fn wrap_with_related(self, related: Vec<Mail>)
+ -> Mail
+ {
+ let mut bodies = related;
+ bodies.insert(0, self);
+ new_multipart(&RELATED, bodies)
+ }
+
+}
+
+/// Creates a `multipart/<sub_type>` mail with given bodies.
+///
+/// # Panic
+///
+/// If `sub_type` can not be used to create a multipart content
+/// type this will panic.
+fn new_multipart(sub_type: &'static str, bodies: Vec<Mail>)
+ -> Mail
+{
+ let content_type = MediaType::new(MULTIPART, sub_type)
+ .unwrap();
+ Mail::new_multipart_mail(content_type, bodies)
+} \ No newline at end of file