Repository: lettre/lettre (language: rust)

Issue:
Make `ContentType::from_mime` public
**Is your feature request related to a problem? Please describe.**

Version 0.10.0-beta.4 removed the ability to instantiate a new `ContentType` directly from a `mime::Mime`. The `from_mime()` method exists but is crate-private.

I'm using it for attachments/inline images:

```rust
let attachment_parts = attachments
    .into_iter()
    .map(|attachment| {
        SinglePart::builder()
            .header(ContentTransferEncoding::Base64)
            .content_type(ContentType(attachment.content_type)) // <---- no longer compiles
            .header(header::ContentDisposition::attachment(attachment.filename))
            .body(attachment.bytes)
    }).collect::<Vec<_>>();
```

Compilation error:

```
error[E0423]: cannot initialize a tuple struct which contains private fields
  --> src/email/smtp_email_service.rs:48:35
   |
48 |                     .content_type(ContentType(attachment.content_type))
   |                                   ^^^^^^^^^^^ constructor is not visible here due to private fields
```

**Describe the solution you'd like**

Make `ContentType::from_mime()` public. `ContentType`s with arbitrary `Mime` values are required and already able to be constructed using `ContentType::parse()`, so I don't see any downside of making `from_mime` public.

Current definition:
```rust
pub(crate) const fn from_mime(mime: Mime) -> Self {
    Self(mime)
}
```

**Describe alternatives you've considered**

The current workaround is to convert a `mime::Mime` to a string and use `ContentType::parse()` which coverts it back to a `Mime` internally anyway. This is fine for now, just not very ergonomic when already using `Mime`s.

```rust
/// Parse `s` into `ContentType`
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
    Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
}
```

E.g.

```rust

let attachment_parts = attachments
    .into_iter()
    .map(|attachment| {
        SinglePart::builder()
            .header(ContentTransferEncoding::Base64)
            .content_type(ContentType::parse(attachment.content_type.to_string()).unwrap()) // <---- can't use ? due to map(); infallible but still not great
            .header(header::ContentDisposition::attachment(attachment.filename))
            .body(attachment.bytes)
    }).collect::<Vec<_>>();
```

**Additional context**

Thank you for maintaining and improving `lettre`.



Discussion / hints:
I guess with all of the examples suggesting to call `str::parse()` we didn't think too much about use-cases where we already have an instance of `Mime`. On the other hand the decision of having it as a public dependency would be much easier if `mime` finally decided to go for a 1.0 release, which seems to be under discussion for 4 years... https://github.com/hyperium/mime/issues/52

The most flexible option here could be to have a `impl From<Mime> for ContentType` behind a `mime03` feature flag. In the future, when `mime` 1.0 will have been released, we will add a `impl From<Mime> for ContentType` for mime 1.0, but keeping the mime 0.3 support. This way it wouldn't be a breaking change to upgrade `mime` in a patch release. Because the first `impl` would be behind the `mime03` feature flag, only those users would get a duplicate dependency version.
I didn't think about `Mime` version conflicts. It looks like it hasn't been updated in over a year (https://github.com/hyperium/mime/issues/135) so having it as a public dependency would be unlikely to break user code or require frequent releases to maintain parity etc. but I agree that a feature flag does have the most flexibility. 

Is there anywhere else in `lettre` that would be affected / benefit from this change? From a quick glance it looks like `ContentType` is the only place.
> I didn't think about `Mime` version conflicts. It looks like it hasn't been updated in over a year ([hyperium/mime#135](https://github.com/hyperium/mime/issues/135)) so having it as a public dependency would be unlikely to break user code or require frequent releases to maintain parity etc. but I agree that a feature flag does have the most flexibility.

While it is unlikely, the version after 0.10 could be 1.0, so we already want to start thinking about [this kind of API stability issues](https://rust-lang.github.io/api-guidelines/necessities.html#public-dependencies-of-a-stable-crate-are-stable-c-stable).

> Is there anywhere else in `lettre` that would be affected / benefit from this change? From a quick glance it looks like `ContentType` is the only place.

Correct

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index e75a6b158..52aa39bb4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -78,6 +78,7 @@ name = "transport_smtp"
 [features]
 default = ["smtp-transport", "pool", "native-tls", "hostname", "r2d2", "builder"]
 builder = ["httpdate", "mime", "base64", "fastrand", "quoted_printable"]
+mime03 = ["mime"]
 
 # transports
 file-transport = ["uuid"]
diff --git a/src/lib.rs b/src/lib.rs
index 5932916d1..58bc6bb79 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -85,16 +85,19 @@
 //!
 //! * **serde**: Serialization/Deserialization of entities
 //! * **tracing**: Logging using the `tracing` crate
+//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
 //!
 //! [`SMTP`]: crate::transport::smtp
 //! [`sendmail`]: crate::transport::sendmail
 //! [`file`]: crate::transport::file
+//! [`ContentType`]: crate::message::header::ContentType
 //! [tokio]: https://docs.rs/tokio/1
 //! [async-std]: https://docs.rs/async-std/1
 //! [ring]: https://github.com/briansmith/ring#ring
 //! [ring-support]: https://github.com/briansmith/ring#online-automated-testing
 //! [Tokio 1.x]: https://docs.rs/tokio/1
 //! [async-std 1.x]: https://docs.rs/async-std/1
+//! [mime 0.3]: https://docs.rs/mime/0.3
 
 #![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-rc.3")]
 #![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
diff --git a/src/message/header/content_type.rs b/src/message/header/content_type.rs
index 2ac6cc913..f7f21e598 100644
--- a/src/message/header/content_type.rs
+++ b/src/message/header/content_type.rs
@@ -67,6 +67,14 @@ impl FromStr for ContentType {
     }
 }
 
+#[cfg(feature = "mime03")]
+#[cfg_attr(docsrs, doc(cfg(feature = "mime03")))]
+impl From<Mime> for ContentType {
+    fn from(mime: Mime) -> Self {
+        Self::from_mime(mime)
+    }
+}
+
 /// An error occurred while trying to [`ContentType::parse`].
 #[derive(Debug)]
 pub struct ContentTypeErr(mime::FromStrError);


Test patch: