diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4c778cf..58a7f909 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+## [0.2.0] - 2021-03-10
+
+### Added
+
+- STARTTLS support [#32]
+- Flags [#25]
+
+### Changed
+
+- JSON support [#18]
+
 ## [0.1.0] - 2021-01-17
 
 ### Added
@@ -30,7 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Password from command [#22]
 - Set up README [#20]
 
-[unreleased]: https://github.com/soywod/himalaya/compare/v0.1.0...HEAD
+[unreleased]: https://github.com/soywod/himalaya/compare/v0.2.0...HEAD
+[0.2.0]: https://github.com/soywod/himalaya/compare/v0.2.0...v0.1.0
 [0.1.0]: https://github.com/soywod/himalaya/releases/tag/v0.1.0
 
 [#1]: https://github.com/soywod/himalaya/issues/1
@@ -48,7 +60,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 [#15]: https://github.com/soywod/himalaya/issues/15
 [#16]: https://github.com/soywod/himalaya/issues/16
 [#17]: https://github.com/soywod/himalaya/issues/17
+[#18]: https://github.com/soywod/himalaya/issues/18
 [#19]: https://github.com/soywod/himalaya/issues/19
 [#20]: https://github.com/soywod/himalaya/issues/20
 [#21]: https://github.com/soywod/himalaya/issues/21
 [#22]: https://github.com/soywod/himalaya/issues/22
+[#25]: https://github.com/soywod/himalaya/issues/25
+[#32]: https://github.com/soywod/himalaya/issues/32
diff --git a/Cargo.lock b/Cargo.lock
index 2a1baa5c..ad7515aa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -228,17 +228,19 @@ dependencies = [
 
 [[package]]
 name = "himalaya"
-version = "0.1.0"
+version = "0.2.0"
 dependencies = [
  "clap",
  "imap",
  "lettre",
  "mailparse",
  "native-tls",
+ "rfc2047-decoder",
  "serde",
  "serde_json",
  "terminal_size",
  "toml",
+ "uuid",
 ]
 
 [[package]]
@@ -707,6 +709,17 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rfc2047-decoder"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ecf2ba387f446155e26796aabb727e9ae1427dd13ac9cc21773a3fbda19d77"
+dependencies = [
+ "base64 0.13.0",
+ "charset",
+ "quoted_printable",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.5"
diff --git a/Cargo.toml b/Cargo.toml
index 3529c02a..7636e37b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "himalaya"
 description = "📫 Minimalist CLI email client"
-version = "0.1.0"
+version = "0.2.0"
 authors = ["soywod <clement.douin@posteo.net>"]
 edition = "2018"
 
@@ -11,7 +11,9 @@ imap = "2.4.0"
 lettre = "0.10.0-alpha.4"
 mailparse = "0.13.1"
 native-tls = "0.2"
+rfc2047-decoder = "0.1.2"
 serde = { version = "1.0.118", features = ["derive"] }
 serde_json = "1.0.61"
 terminal_size = "0.1.15"
 toml = "0.5.8"
+uuid = { version = "0.8", features = ["v4"] }
diff --git a/README.md b/README.md
index 2769c060..7e2f3c79 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# 📫 Himalaya [![gh-actions](https://github.com/soywod/himalaya/workflows/deployment/badge.svg)](https://github.com/soywod/himalaya/actions?query=workflow%3Adeployment)
+# 📫 Himalaya [WIP] [![gh-actions](https://github.com/soywod/himalaya/workflows/deployment/badge.svg)](https://github.com/soywod/himalaya/actions?query=workflow%3Adeployment)
 
 Minimalist CLI email client, written in Rust.
 
@@ -8,28 +8,30 @@ Minimalist CLI email client, written in Rust.
 
 * [Motivation](#motivation)
 * [Installation](#installation)
+* [Configuration](#configuration)
 * [Usage](#usage)
   * [List mailboxes](#list-mailboxes)
-  * [List emails](#list-emails)
-  * [Search emails](#search-emails)
-  * [Download email attachments](#download-email-attachments)
-  * [Read email](#read-email)
-  * [Reply email](#reply-email)
-  * [Forward email](#forward-email)
+  * [List messages](#list-messages)
+  * [Search messages](#search-messages)
+  * [Download attachments](#download-attachments)
+  * [Read a message](#read-a-message)
+  * [Write a new message](#write-a-new-message)
+  * [Reply to a message](#reply-to-a-message)
+  * [Forward a message](#forward-a-message)
 * [License](https://github.com/soywod/himalaya/blob/master/LICENSE)
 * [Changelog](https://github.com/soywod/himalaya/blob/master/CHANGELOG.md)
 * [Credits](#credits)
 
 ## Motivation
 
-Bringing emails to your terminal is a pain. The mainstream TUI, (neo)mutt,
-takes time to configure. The default mapping is not intuitive when coming from
-the Vim environment. It is even scary to use at the beginning, since you are
+Bringing emails to the terminal is a pain. The mainstream TUI, (neo)mutt, takes
+time to configure. The default mapping is not intuitive when coming from the
+Vim environment. It is even scary to use at the beginning, since you are
 dealing with sensitive data!
 
-The aim of Himalaya is to extract the email logic into a simple CLI API that
-can be used either directly for the terminal or from various interfaces. It
-gives users more flexibility.
+The aim of Himalaya is to extract the email logic into a simple (yet solid) CLI
+API that can be used either directly from the terminal or UIs. It gives users
+more flexibility.
 
 ## Installation
 
@@ -46,7 +48,7 @@ more information.*
 # ~/.config/himalaya/config.toml
 
 name = "Your full name"
-downloads_dir = "/abs/path/to/downloads"
+downloads-dir = "/abs/path/to/downloads"
 
 # Himalaya supports the multi-account
 # Each account should be inside a TOML section
@@ -54,32 +56,32 @@ downloads_dir = "/abs/path/to/downloads"
 default = true
 email = "my.email@gmail.com"
 
-imap_host = "imap.gmail.com"
-imap_port = 993
-imap_login = "p.durant@gmail.test.com"
-imap_passwd_cmd = "pass show gmail"
+imap-host = "imap.gmail.com"
+imap-port = 993
+imap-login = "test@gmail.com"
+imap-passwd_cmd = "pass show gmail"
 
-smtp_host = "smtp.gmail.com"
-smtp_port = 487
-smtp_login = "p.durant@gmail.test.com"
-smtp_passwd_cmd = "pass show gmail"
+smtp-host = "smtp.gmail.com"
+smtp-port = 487
+smtp-login = "test@gmail.com"
+smtp-passwd_cmd = "pass show gmail"
 
 [posteo]
 name = "Your overriden full name"
-downloads_dir = "/abs/path/to/overriden/downloads"
-email = "my.email@posteo.net"
+downloads-dir = "/abs/path/to/overriden/downloads"
+email = "test@posteo.net"
 
-imap_host = "posteo.de"
-imap_port = 993
-imap_login = "my.email@posteo.net"
-imap_passwd_cmd = "security find-internet-password -gs posteo -w"
+imap-host = "posteo.de"
+imap-port = 993
+imap-login = "test@posteo.net"
+imap-passwd_cmd = "security find-internet-password -gs posteo -w"
 
-smtp_host = "posteo.de"
-smtp_port = 487
-smtp_login = "my.email@posteo.net"
-smtp_passwd_cmd = "security find-internet-password -gs posteo -w"
+smtp-host = "posteo.de"
+smtp-port = 487
+smtp-login = "test@posteo.net"
+smtp-passwd_cmd = "security find-internet-password -gs posteo -w"
 
-# [other account]
+# [other accounts]
 # ...
 ```
 
@@ -89,7 +91,7 @@ more information.*
 ## Usage
 
 ```
-Himalaya 0.1.0
+Himalaya 0.2.0
 soywod <clement.douin@posteo.net>
 📫 Minimalist CLI email client
 
@@ -101,7 +103,8 @@ FLAGS:
     -V, --version    Prints version information
 
 OPTIONS:
-    -a, --account <STRING>    Name of the config file to use
+    -a, --account <STRING>    Name of the account to use
+    -o, --output <STRING>     Format of the output to print [default: text]  [possible values: text, json]
 
 SUBCOMMANDS:
     attachments    Downloads all attachments from an email
@@ -111,7 +114,10 @@ SUBCOMMANDS:
     mailboxes      Lists all available mailboxes
     read           Reads text bodies of an email
     reply          Answers to an email
+    save           Saves a raw message in the given mailbox
     search         Lists emails matching the given IMAP query
+    send           Sends a raw message
+    template       Generates a message template
     write          Writes a new email
 ```
 
@@ -120,191 +126,91 @@ information.*
 
 ### List mailboxes
 
-![image](https://user-images.githubusercontent.com/10437171/104848169-0e432000-58e4-11eb-8410-05f0404c0d99.png)
-
-```
-himalaya-mailboxes 
-Lists all available mailboxes
+Shows mailboxes in a basic table.
 
-USAGE:
-    himalaya mailboxes
-
-FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
-```
+![image](https://user-images.githubusercontent.com/10437171/104848169-0e432000-58e4-11eb-8410-05f0404c0d99.png)
 
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:mailboxes)
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:list-mailboxes)
 for more information.*
 
-### List emails
-
-![image](https://user-images.githubusercontent.com/10437171/104848096-aee51000-58e3-11eb-8d99-bcfab5ca28ba.png)
-
-```
-himalaya-list 
-Lists emails sorted by arrival date
-
-USAGE:
-    himalaya list [OPTIONS]
-
-FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
-
-OPTIONS:
-    -m, --mailbox <STRING>    Name of the mailbox [default: INBOX]
-    -p, --page <INT>          Page number [default: 0]
-    -s, --size <INT>          Page size [default: 10]
-```
-
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:list) for
-more information.*
+### List messages
 
-### Search emails
+Shows messages in a basic table.
 
 ![image](https://user-images.githubusercontent.com/10437171/104848096-aee51000-58e3-11eb-8d99-bcfab5ca28ba.png)
 
-```
-himalaya-search 
-Lists emails matching the given IMAP query
-
-USAGE:
-    himalaya search [OPTIONS] <QUERY>...
-
-FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
-
-OPTIONS:
-    -m, --mailbox <STRING>    Name of the mailbox [default: INBOX]
-    -p, --page <INT>          Page number [default: 0]
-    -s, --size <INT>          Page size [default: 10]
-
-ARGS:
-    <QUERY>...    IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)
-```
-
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:search) for
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:list-messages) for
 more information.*
 
-### Download email attachments
+### Search messages
 
-![image](https://user-images.githubusercontent.com/10437171/104848278-890c3b00-58e4-11eb-9b5c-48807c04f762.png)
+Shows filtered messages in a basic table. The query should follow the
+[RFC-3501](https://tools.ietf.org/html/rfc3501#section-6.4.4).
 
-```
-himalaya-attachments 
-Downloads all attachments from an email
+![image](https://user-images.githubusercontent.com/10437171/110698977-9d86f880-81ee-11eb-8990-0ca89c7d4640.png)
 
-USAGE:
-    himalaya attachments [OPTIONS] <UID>
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:search-messages) for
+more information.*
 
-FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
+### Download attachments
 
-OPTIONS:
-    -m, --mailbox <STRING>    Name of the mailbox [default: INBOX]
+Downloads all attachments directly to the [`downloads-dir`](#configuration).
 
-ARGS:
-    <UID>    UID of the email
-```
+![image](https://user-images.githubusercontent.com/10437171/104848278-890c3b00-58e4-11eb-9b5c-48807c04f762.png)
 
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:attachments)
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:download-attachments)
 for more information.*
 
-### Read email
+### Read a message
 
-```
-himalaya-read 
-Reads text bodies of an email
+Shows the text content of a message (`text/plain` if exists, otherwise
+`text/html`). Can be overriden by the `--mime-type` option.
 
-USAGE:
-    himalaya read [OPTIONS] <UID>
-
-FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
+![image](https://user-images.githubusercontent.com/10437171/110701369-5d754500-81f1-11eb-932f-94c2ca8db068.png)
 
-OPTIONS:
-    -m, --mailbox <STRING>      Name of the mailbox [default: INBOX]
-    -t, --mime-type <STRING>    MIME type to use [default: plain]  [possible values: plain, html]
-
-ARGS:
-    <UID>    UID of the email
-```
-
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:read) for
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:read-a-message) for
 more information.*
 
-### Write email
-
-```
-himalaya-write 
-Writes a new email
+### Write a new message
 
-USAGE:
-    himalaya write
+Opens your default editor (from the `$EDITOR` environment variable) to compose
+a new message.
 
-FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
+```bash
+himalaya write
 ```
 
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:write) for
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:write-a-new-message) for
 more information.*
 
-### Reply email
-
-```
-himalaya-reply 
-Answers to an email
+### Reply to a message
 
-USAGE:
-    himalaya reply [FLAGS] [OPTIONS] <UID>
+Opens your default editor to reply to a message.
 
-FLAGS:
-    -h, --help       Prints help information
-    -a, --all        Including all recipients
-    -V, --version    Prints version information
-
-OPTIONS:
-    -m, --mailbox <STRING>    Name of the mailbox [default: INBOX]
-
-ARGS:
-    <UID>    UID of the email
+```bash
+himalaya reply --all 5123
 ```
 
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:reply) for
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:reply-to-a-message) for
 more information.*
 
-### Forward email
+### Forward a message
 
-```
-himalaya-forward 
-Forwards an email
-
-USAGE:
-    himalaya forward [OPTIONS] <UID>
-
-FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
+Opens your default editor to forward a message.
 
-OPTIONS:
-    -m, --mailbox <STRING>    Name of the mailbox [default: INBOX]
-
-ARGS:
-    <UID>    UID of the email
+```bash
+himalaya forward 5123
 ```
 
-*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:forward) for
+*See [wiki section](https://github.com/soywod/himalaya/wiki/Usage:forward-a-message) for
 more information.*
 
 ## Credits
 
 - [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
 - [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
-- [Neomutt](https://neomutt.org/)
-- [Alpine](http://alpine.x10host.com/alpine/alpine-info/)
-- [rust-imap](https://github.com/jonhoo/rust-imap)
+- [isync](https://isync.sourceforge.io/), an email synchronizer for offline usage
+- [NeoMutt](https://neomutt.org/), an email terminal user interface
+- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface
+- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync
+- [rust-imap](https://github.com/jonhoo/rust-imap), a rust IMAP lib
diff --git a/src/config.rs b/src/config.rs
index b8915d8d..0d1b786e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -10,7 +10,7 @@ use std::{
 };
 use toml;
 
-use crate::io::run_cmd;
+use crate::output::{self, run_cmd};
 
 // Error wrapper
 
@@ -23,8 +23,7 @@ pub enum Error {
     GetPathNotFoundError,
     GetAccountNotFoundError(String),
     GetAccountDefaultNotFoundError,
-    ParseImapPasswdUtf8Error,
-    ParseSmtpPasswdUtf8Error,
+    OutputError(output::Error),
 }
 
 impl fmt::Display for Error {
@@ -39,8 +38,7 @@ impl fmt::Display for Error {
             Error::GetPathNotFoundError => write!(f, "path not found"),
             Error::GetAccountNotFoundError(account) => write!(f, "account {} not found", account),
             Error::GetAccountDefaultNotFoundError => write!(f, "no default account found"),
-            Error::ParseImapPasswdUtf8Error => write!(f, "imap passwd invalid utf8"),
-            Error::ParseSmtpPasswdUtf8Error => write!(f, "smtp passwd invalid utf8"),
+            Error::OutputError(err) => err.fmt(f),
         }
     }
 }
@@ -63,6 +61,12 @@ impl From<env::VarError> for Error {
     }
 }
 
+impl From<output::Error> for Error {
+    fn from(err: output::Error) -> Error {
+        Error::OutputError(err)
+    }
+}
+
 // Result wrapper
 
 type Result<T> = result::Result<T, Error>;
@@ -70,6 +74,7 @@ type Result<T> = result::Result<T, Error>;
 // Account
 
 #[derive(Debug, Deserialize)]
+#[serde(rename_all = "kebab-case")]
 pub struct Account {
     // Override
     pub name: Option<String>,
@@ -81,42 +86,41 @@ pub struct Account {
 
     pub imap_host: String,
     pub imap_port: u16,
+    pub imap_starttls: Option<bool>,
     pub imap_login: String,
     pub imap_passwd_cmd: String,
 
     pub smtp_host: String,
     pub smtp_port: u16,
+    pub smtp_starttls: Option<bool>,
     pub smtp_login: String,
     pub smtp_passwd_cmd: String,
 }
 
 impl Account {
+    pub fn imap_addr(&self) -> (&str, u16) {
+        (&self.imap_host, self.imap_port)
+    }
+
     pub fn imap_passwd(&self) -> Result<String> {
-        let cmd = run_cmd(&self.imap_passwd_cmd)?;
-        let passwd = String::from_utf8(cmd.stdout);
-        let passwd = passwd.map_err(|_| Error::ParseImapPasswdUtf8Error)?;
+        let passwd = run_cmd(&self.imap_passwd_cmd)?;
         let passwd = passwd.trim_end_matches("\n").to_owned();
 
         Ok(passwd)
     }
 
     pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
-        let cmd = run_cmd(&self.smtp_passwd_cmd)?;
-        let passwd = String::from_utf8(cmd.stdout);
-        let passwd = passwd.map_err(|_| Error::ParseImapPasswdUtf8Error)?;
+        let passwd = run_cmd(&self.smtp_passwd_cmd)?;
         let passwd = passwd.trim_end_matches("\n").to_owned();
 
         Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
     }
-
-    pub fn imap_addr(&self) -> (&str, u16) {
-        (&self.imap_host, self.imap_port)
-    }
 }
 
 // Config
 
 #[derive(Debug, Deserialize)]
+#[serde(rename_all = "kebab-case")]
 pub struct Config {
     pub name: String,
     pub downloads_dir: Option<PathBuf>,
@@ -167,7 +171,7 @@ impl Config {
         Ok(toml::from_slice(&content)?)
     }
 
-    pub fn get_account(&self, name: Option<&str>) -> Result<&Account> {
+    pub fn find_account_by_name(&self, name: Option<&str>) -> Result<&Account> {
         match name {
             Some(name) => self
                 .accounts
diff --git a/src/imap.rs b/src/imap.rs
index 2b99c409..4f9e6a2c 100644
--- a/src/imap.rs
+++ b/src/imap.rs
@@ -3,8 +3,8 @@ use native_tls::{self, TlsConnector, TlsStream};
 use std::{fmt, net::TcpStream, result};
 
 use crate::config::{self, Account};
-use crate::mbox::Mbox;
-use crate::msg::Msg;
+use crate::mbox::{Mbox, Mboxes};
+use crate::msg::{Msg, Msgs};
 
 // Error wrapper
 
@@ -80,7 +80,10 @@ pub struct ImapConnector<'a> {
 impl<'a> ImapConnector<'a> {
     pub fn new(account: &'a Account) -> Result<Self> {
         let tls = TlsConnector::new()?;
-        let client = imap::connect(account.imap_addr(), &account.imap_host, &tls)?;
+        let client = match account.imap_starttls {
+            Some(true) => imap::connect_starttls(account.imap_addr(), &account.imap_host, &tls),
+            _ => imap::connect(account.imap_addr(), &account.imap_host, &tls),
+        }?;
         let sess = client
             .login(&account.imap_login, &account.imap_passwd()?)
             .map_err(|res| res.0)?;
@@ -88,13 +91,13 @@ impl<'a> ImapConnector<'a> {
         Ok(Self { account, sess })
     }
 
-    pub fn close(&mut self) {
-        match self.sess.close() {
+    pub fn logout(&mut self) {
+        match self.sess.logout() {
             _ => (),
         }
     }
 
-    pub fn list_mboxes(&mut self) -> Result<Vec<Mbox>> {
+    pub fn list_mboxes(&mut self) -> Result<Mboxes> {
         let mboxes = self
             .sess
             .list(Some(""), Some("*"))?
@@ -102,24 +105,24 @@ impl<'a> ImapConnector<'a> {
             .map(Mbox::from_name)
             .collect::<Vec<_>>();
 
-        Ok(mboxes)
+        Ok(Mboxes(mboxes))
     }
 
-    pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result<Vec<Msg>> {
+    pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result<Msgs> {
         let last_seq = self.sess.select(mbox)?.exists;
-        let begin = last_seq - (page * page_size);
-        let end = begin - (page_size - 1);
+        let begin = last_seq - page * page_size;
+        let end = begin - (begin - 1).min(page_size - 1);
         let range = format!("{}:{}", begin, end);
 
         let msgs = self
             .sess
-            .fetch(range, "(UID BODY.PEEK[])")?
+            .fetch(range, "(UID FLAGS ENVELOPE INTERNALDATE)")?
             .iter()
             .rev()
             .map(Msg::from)
             .collect::<Vec<_>>();
 
-        Ok(msgs)
+        Ok(Msgs(msgs))
     }
 
     pub fn search_msgs(
@@ -128,7 +131,7 @@ impl<'a> ImapConnector<'a> {
         query: &str,
         page_size: &usize,
         page: &usize,
-    ) -> Result<Vec<Msg>> {
+    ) -> Result<Msgs> {
         self.sess.select(mbox)?;
 
         let begin = page * page_size;
@@ -143,13 +146,12 @@ impl<'a> ImapConnector<'a> {
 
         let msgs = self
             .sess
-            .fetch(range, "(UID BODY.PEEK[])")?
+            .fetch(range, "(UID ENVELOPE INTERNALDATE)")?
             .iter()
-            .rev()
             .map(Msg::from)
             .collect::<Vec<_>>();
 
-        Ok(msgs)
+        Ok(Msgs(msgs))
     }
 
     pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result<Vec<u8>> {
diff --git a/src/io.rs b/src/input.rs
similarity index 87%
rename from src/io.rs
rename to src/input.rs
index 80de57d1..e59418cb 100644
--- a/src/io.rs
+++ b/src/input.rs
@@ -2,7 +2,7 @@ use std::{
     env, fmt,
     fs::{remove_file, File},
     io::{self, Read, Write},
-    process::{Command, Output},
+    process::Command,
     result,
 };
 
@@ -78,11 +78,3 @@ pub fn ask_for_confirmation(prompt: &str) -> Result<()> {
         _ => Err(Error::AskForConfirmationDeniedError),
     }
 }
-
-pub fn run_cmd(cmd: &str) -> io::Result<Output> {
-    if cfg!(target_os = "windows") {
-        Command::new("cmd").args(&["/C", cmd]).output()
-    } else {
-        Command::new("sh").arg("-c").arg(cmd).output()
-    }
-}
diff --git a/src/main.rs b/src/main.rs
index 14559907..dc9eb803 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,8 +1,9 @@
 mod config;
 mod imap;
-mod io;
+mod input;
 mod mbox;
 mod msg;
+mod output;
 mod smtp;
 mod table;
 
@@ -11,8 +12,8 @@ use std::{fmt, fs, process::exit, result};
 
 use crate::config::Config;
 use crate::imap::ImapConnector;
-use crate::msg::Msg;
-use crate::table::DisplayTable;
+use crate::msg::{Attachments, Msg, ReadableMsg};
+use crate::output::print;
 
 const DEFAULT_PAGE_SIZE: usize = 10;
 const DEFAULT_PAGE: usize = 0;
@@ -20,7 +21,8 @@ const DEFAULT_PAGE: usize = 0;
 #[derive(Debug)]
 pub enum Error {
     ConfigError(config::Error),
-    IoError(io::Error),
+    InputError(input::Error),
+    OutputError(output::Error),
     MsgError(msg::Error),
     ImapError(imap::Error),
     SmtpError(smtp::Error),
@@ -30,7 +32,8 @@ impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
             Error::ConfigError(err) => err.fmt(f),
-            Error::IoError(err) => err.fmt(f),
+            Error::InputError(err) => err.fmt(f),
+            Error::OutputError(err) => err.fmt(f),
             Error::MsgError(err) => err.fmt(f),
             Error::ImapError(err) => err.fmt(f),
             Error::SmtpError(err) => err.fmt(f),
@@ -44,9 +47,15 @@ impl From<config::Error> for Error {
     }
 }
 
-impl From<crate::io::Error> for Error {
-    fn from(err: crate::io::Error) -> Error {
-        Error::IoError(err)
+impl From<input::Error> for Error {
+    fn from(err: input::Error) -> Error {
+        Error::InputError(err)
+    }
+}
+
+impl From<output::Error> for Error {
+    fn from(err: output::Error) -> Error {
+        Error::OutputError(err)
     }
 }
 
@@ -90,6 +99,13 @@ fn uid_arg() -> Arg<'static, 'static> {
         .required(true)
 }
 
+fn reply_all_arg() -> Arg<'static, 'static> {
+    Arg::with_name("reply-all")
+        .help("Includes all recipients")
+        .short("a")
+        .long("all")
+}
+
 fn page_size_arg<'a>(default: &'a str) -> Arg<'a, 'a> {
     Arg::with_name("size")
         .help("Page size")
@@ -113,20 +129,29 @@ fn run() -> Result<()> {
     let default_page_str = &DEFAULT_PAGE.to_string();
 
     let matches = App::new("Himalaya")
-        .version("0.1.0")
+        .version("0.2.0")
         .about("📫 Minimalist CLI email client")
         .author("soywod <clement.douin@posteo.net>")
         .setting(AppSettings::ArgRequiredElseHelp)
+        .arg(
+            Arg::with_name("output")
+                .long("output")
+                .short("o")
+                .help("Format of the output to print")
+                .value_name("STRING")
+                .possible_values(&["text", "json"])
+                .default_value("text"),
+        )
         .arg(
             Arg::with_name("account")
                 .long("account")
                 .short("a")
-                .help("Name of the config file to use")
+                .help("Name of the account to use")
                 .value_name("STRING"),
         )
         .subcommand(
             SubCommand::with_name("mailboxes")
-                .aliases(&["mboxes", "mb", "m"])
+                .aliases(&["mboxes", "mbox", "mb", "m"])
                 .about("Lists all available mailboxes"),
         )
         .subcommand(
@@ -170,7 +195,7 @@ fn run() -> Result<()> {
         )
         .subcommand(
             SubCommand::with_name("attachments")
-                .aliases(&["attach", "a"])
+                .aliases(&["attach", "att", "a"])
                 .about("Downloads all attachments from an email")
                 .arg(uid_arg())
                 .arg(mailbox_arg()),
@@ -182,12 +207,7 @@ fn run() -> Result<()> {
                 .about("Answers to an email")
                 .arg(uid_arg())
                 .arg(mailbox_arg())
-                .arg(
-                    Arg::with_name("reply-all")
-                        .help("Including all recipients")
-                        .short("a")
-                        .long("all"),
-                ),
+                .arg(reply_all_arg()),
         )
         .subcommand(
             SubCommand::with_name("forward")
@@ -196,24 +216,62 @@ fn run() -> Result<()> {
                 .arg(uid_arg())
                 .arg(mailbox_arg()),
         )
+        .subcommand(
+            SubCommand::with_name("send")
+                .about("Sends a raw message")
+                .arg(Arg::with_name("message").raw(true)),
+        )
+        .subcommand(
+            SubCommand::with_name("save")
+                .about("Saves a raw message in the given mailbox")
+                .arg(mailbox_arg())
+                .arg(Arg::with_name("message").raw(true)),
+        )
+        .subcommand(
+            SubCommand::with_name("template")
+                .aliases(&["tpl", "t"])
+                .about("Generates a message template")
+                .subcommand(
+                    SubCommand::with_name("new")
+                        .aliases(&["n"])
+                        .about("Generates a new message template")
+                        .arg(mailbox_arg()),
+                )
+                .subcommand(
+                    SubCommand::with_name("reply")
+                        .aliases(&["rep", "r"])
+                        .about("Generates a reply message template")
+                        .arg(uid_arg())
+                        .arg(mailbox_arg())
+                        .arg(reply_all_arg()),
+                )
+                .subcommand(
+                    SubCommand::with_name("forward")
+                        .aliases(&["fwd", "fw", "f"])
+                        .about("Generates a forward message template")
+                        .arg(uid_arg())
+                        .arg(mailbox_arg()),
+                ),
+        )
         .get_matches();
 
     let account_name = matches.value_of("account");
+    let output_type = matches.value_of("output").unwrap().to_owned();
 
     if let Some(_) = matches.subcommand_matches("mailboxes") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
 
         let mboxes = imap_conn.list_mboxes()?;
-        println!("{}", mboxes.to_table());
+        print(&output_type, mboxes)?;
 
-        imap_conn.close();
+        imap_conn.logout();
     }
 
     if let Some(matches) = matches.subcommand_matches("list") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
 
         let mbox = matches.value_of("mailbox").unwrap();
@@ -229,16 +287,15 @@ fn run() -> Result<()> {
             .unwrap_or(DEFAULT_PAGE as u32);
 
         let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?;
-        println!("{}", msgs.to_table());
+        print(&output_type, msgs)?;
 
-        imap_conn.close();
+        imap_conn.logout();
     }
 
     if let Some(matches) = matches.subcommand_matches("search") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
-
         let mbox = matches.value_of("mailbox").unwrap();
         let page_size: usize = matches
             .value_of("size")
@@ -276,75 +333,124 @@ fn run() -> Result<()> {
             .join(" ");
 
         let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?;
-        println!("{}", msgs.to_table());
+        print(&output_type, msgs)?;
 
-        imap_conn.close();
+        imap_conn.logout();
     }
 
     if let Some(matches) = matches.subcommand_matches("read") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
 
         let mbox = matches.value_of("mailbox").unwrap();
         let uid = matches.value_of("uid").unwrap();
         let mime = format!("text/{}", matches.value_of("mime-type").unwrap());
 
-        let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
-        let text_bodies = msg.text_bodies(&mime)?;
-        println!("{}", text_bodies);
+        let msg = imap_conn.read_msg(&mbox, &uid)?;
+        let msg = ReadableMsg::from_bytes(&mime, &msg)?;
 
-        imap_conn.close();
+        print(&output_type, msg)?;
+        imap_conn.logout();
     }
 
     if let Some(matches) = matches.subcommand_matches("attachments") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
 
         let mbox = matches.value_of("mailbox").unwrap();
         let uid = matches.value_of("uid").unwrap();
 
-        let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
-        let parts = msg.extract_attachments()?;
-
-        if parts.is_empty() {
-            println!("No attachment found for message {}", uid);
-        } else {
-            println!("{} attachment(s) found for message {}", parts.len(), uid);
-            parts.iter().for_each(|(filename, bytes)| {
-                let filepath = config.downloads_filepath(&account, &filename);
-                println!("Downloading {} …", filename);
-                fs::write(filepath, bytes).unwrap()
-            });
-            println!("Done!");
+        let msg = imap_conn.read_msg(&mbox, &uid)?;
+        let attachments = Attachments::from_bytes(&msg)?;
+
+        match output_type.as_str() {
+            "text" => {
+                println!(
+                    "{} attachment(s) found for message {}",
+                    attachments.0.len(),
+                    uid
+                );
+
+                attachments.0.iter().for_each(|attachment| {
+                    let filepath = config.downloads_filepath(&account, &attachment.filename);
+                    println!("Downloading {}…", &attachment.filename);
+                    fs::write(filepath, &attachment.raw).unwrap()
+                });
+
+                println!("Done!");
+            }
+            "json" => {
+                attachments.0.iter().for_each(|attachment| {
+                    let filepath = config.downloads_filepath(&account, &attachment.filename);
+                    fs::write(filepath, &attachment.raw).unwrap()
+                });
+
+                print!("{{}}");
+            }
+            _ => (),
         }
 
-        imap_conn.close();
+        imap_conn.logout();
     }
 
     if let Some(_) = matches.subcommand_matches("write") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
-
         let tpl = Msg::build_new_tpl(&config, &account)?;
-        let content = io::open_editor_with_tpl(&tpl.as_bytes())?;
+        let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?;
         let msg = Msg::from(content);
 
-        io::ask_for_confirmation("Send the message?")?;
+        input::ask_for_confirmation("Send the message?")?;
 
-        println!("Sending …");
+        println!("Sending…");
         smtp::send(&account, &msg.to_sendable_msg()?)?;
         imap_conn.append_msg("Sent", &msg.to_vec()?)?;
         println!("Done!");
 
-        imap_conn.close();
+        imap_conn.logout();
+    }
+
+    if let Some(matches) = matches.subcommand_matches("template") {
+        let config = Config::new_from_file()?;
+        let account = config.find_account_by_name(account_name)?;
+        let mut imap_conn = ImapConnector::new(&account)?;
+
+        if let Some(_) = matches.subcommand_matches("new") {
+            let tpl = Msg::build_new_tpl(&config, &account)?;
+            print(&output_type, &tpl)?;
+        }
+
+        if let Some(matches) = matches.subcommand_matches("reply") {
+            let uid = matches.value_of("uid").unwrap();
+            let mbox = matches.value_of("mailbox").unwrap();
+
+            let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
+            let tpl = if matches.is_present("reply-all") {
+                msg.build_reply_all_tpl(&config, &account)?
+            } else {
+                msg.build_reply_tpl(&config, &account)?
+            };
+
+            print(&output_type, &tpl)?;
+        }
+
+        if let Some(matches) = matches.subcommand_matches("forward") {
+            let uid = matches.value_of("uid").unwrap();
+            let mbox = matches.value_of("mailbox").unwrap();
+
+            let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
+            let tpl = msg.build_forward_tpl(&config, &account)?;
+
+            print(&output_type, &tpl)?;
+        }
     }
 
     if let Some(matches) = matches.subcommand_matches("reply") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
 
         let mbox = matches.value_of("mailbox").unwrap();
@@ -357,22 +463,22 @@ fn run() -> Result<()> {
             msg.build_reply_tpl(&config, &account)?
         };
 
-        let content = io::open_editor_with_tpl(&tpl.as_bytes())?;
+        let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?;
         let msg = Msg::from(content);
 
-        io::ask_for_confirmation("Send the message?")?;
+        input::ask_for_confirmation("Send the message?")?;
 
-        println!("Sending …");
+        println!("Sending…");
         smtp::send(&account, &msg.to_sendable_msg()?)?;
         imap_conn.append_msg("Sent", &msg.to_vec()?)?;
         println!("Done!");
 
-        imap_conn.close();
+        imap_conn.logout();
     }
 
     if let Some(matches) = matches.subcommand_matches("forward") {
         let config = Config::new_from_file()?;
-        let account = config.get_account(account_name)?;
+        let account = config.find_account_by_name(account_name)?;
         let mut imap_conn = ImapConnector::new(&account)?;
 
         let mbox = matches.value_of("mailbox").unwrap();
@@ -380,17 +486,43 @@ fn run() -> Result<()> {
 
         let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
         let tpl = msg.build_forward_tpl(&config, &account)?;
-        let content = io::open_editor_with_tpl(&tpl.as_bytes())?;
+        let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?;
         let msg = Msg::from(content);
 
-        io::ask_for_confirmation("Send the message?")?;
+        input::ask_for_confirmation("Send the message?")?;
 
-        println!("Sending …");
+        println!("Sending…");
         smtp::send(&account, &msg.to_sendable_msg()?)?;
         imap_conn.append_msg("Sent", &msg.to_vec()?)?;
         println!("Done!");
 
-        imap_conn.close();
+        imap_conn.logout();
+    }
+
+    if let Some(matches) = matches.subcommand_matches("send") {
+        let config = Config::new_from_file()?;
+        let account = config.find_account_by_name(account_name)?;
+        let mut imap_conn = ImapConnector::new(&account)?;
+
+        let msg = matches.value_of("message").unwrap();
+        let msg = Msg::from(msg.to_string());
+
+        smtp::send(&account, &msg.to_sendable_msg()?)?;
+        imap_conn.append_msg("Sent", &msg.to_vec()?)?;
+        imap_conn.logout();
+    }
+
+    if let Some(matches) = matches.subcommand_matches("save") {
+        let config = Config::new_from_file()?;
+        let account = config.find_account_by_name(account_name)?;
+        let mut imap_conn = ImapConnector::new(&account)?;
+
+        let mbox = matches.value_of("mailbox").unwrap();
+        let msg = matches.value_of("message").unwrap();
+        let msg = Msg::from(msg.to_string());
+
+        imap_conn.append_msg(mbox, &msg.to_vec()?)?;
+        imap_conn.logout();
     }
 
     Ok(())
diff --git a/src/mbox.rs b/src/mbox.rs
index 8f6290e0..2cb894e9 100644
--- a/src/mbox.rs
+++ b/src/mbox.rs
@@ -1,7 +1,12 @@
 use imap;
+use serde::Serialize;
+use std::fmt;
 
 use crate::table::{self, DisplayRow, DisplayTable};
 
+// Mbox
+
+#[derive(Debug, Serialize)]
 pub struct Mbox {
     pub delim: String,
     pub name: String,
@@ -30,7 +35,12 @@ impl DisplayRow for Mbox {
     }
 }
 
-impl<'a> DisplayTable<'a, Mbox> for Vec<Mbox> {
+// Mboxes
+
+#[derive(Debug, Serialize)]
+pub struct Mboxes(pub Vec<Mbox>);
+
+impl<'a> DisplayTable<'a, Mbox> for Mboxes {
     fn header_row() -> Vec<table::Cell> {
         use crate::table::*;
 
@@ -42,6 +52,12 @@ impl<'a> DisplayTable<'a, Mbox> for Vec<Mbox> {
     }
 
     fn rows(&self) -> &Vec<Mbox> {
-        self
+        &self.0
+    }
+}
+
+impl fmt::Display for Mboxes {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.to_table())
     }
 }
diff --git a/src/msg.rs b/src/msg.rs
index 8a43a127..b7dfbad0 100644
--- a/src/msg.rs
+++ b/src/msg.rs
@@ -1,6 +1,12 @@
 use lettre;
 use mailparse::{self, MailHeaderMap};
+use rfc2047_decoder;
+use serde::{
+    ser::{self, SerializeStruct},
+    Serialize,
+};
 use std::{fmt, result};
+use uuid::Uuid;
 
 use crate::config::{Account, Config};
 use crate::table::{self, DisplayRow, DisplayTable};
@@ -39,41 +45,234 @@ impl From<lettre::error::Error> for Error {
 
 type Result<T> = result::Result<T, Error>;
 
-// Msg
+// Template
 
 #[derive(Debug)]
-pub struct Msg {
-    pub uid: u32,
-    pub flags: Vec<String>,
-    raw: Vec<u8>,
+pub struct Tpl(String);
+
+impl fmt::Display for Tpl {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.0)
+    }
 }
 
-impl From<String> for Msg {
-    fn from(item: String) -> Self {
+impl Serialize for Tpl {
+    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
+    where
+        S: ser::Serializer,
+    {
+        let mut state = serializer.serialize_struct("Tpl", 1)?;
+        state.serialize_field("template", &self.0)?;
+        state.end()
+    }
+}
+
+// Attachments
+
+#[derive(Debug)]
+pub struct Attachment {
+    pub filename: String,
+    pub raw: Vec<u8>,
+}
+
+impl<'a> Attachment {
+    // TODO: put in common with ReadableMsg
+    pub fn from_part(part: &'a mailparse::ParsedMail) -> Self {
         Self {
-            uid: 0,
-            flags: vec![],
-            raw: item.as_bytes().to_vec(),
+            filename: part
+                .get_content_disposition()
+                .params
+                .get("filename")
+                .unwrap_or(&Uuid::new_v4().to_simple().to_string())
+                .to_owned(),
+            raw: part.get_body_raw().unwrap_or_default(),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct Attachments(pub Vec<Attachment>);
+
+impl<'a> Attachments {
+    fn extract_from_part(&'a mut self, part: &'a mailparse::ParsedMail) {
+        if part.subparts.is_empty() {
+            let ctype = part
+                .get_headers()
+                .get_first_value("content-type")
+                .unwrap_or_default();
+
+            if !ctype.starts_with("text") {
+                self.0.push(Attachment::from_part(part));
+            }
+        } else {
+            part.subparts
+                .iter()
+                .for_each(|part| self.extract_from_part(part));
+        }
+    }
+
+    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
+        let msg = mailparse::parse_mail(bytes)?;
+        let mut attachments = Self(vec![]);
+        attachments.extract_from_part(&msg);
+        Ok(attachments)
+    }
+}
+
+// Readable message
+
+#[derive(Debug)]
+pub struct ReadableMsg {
+    pub content: String,
+    pub has_attachment: bool,
+}
+
+impl Serialize for ReadableMsg {
+    fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
+    where
+        S: ser::Serializer,
+    {
+        let mut state = serializer.serialize_struct("ReadableMsg", 2)?;
+        state.serialize_field("content", &self.content)?;
+        state.serialize_field("hasAttachment", if self.has_attachment { &1 } else { &0 })?;
+        state.end()
+    }
+}
+
+impl fmt::Display for ReadableMsg {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.content)
+    }
+}
+
+impl<'a> ReadableMsg {
+    fn flatten_parts(part: &'a mailparse::ParsedMail) -> Vec<&'a mailparse::ParsedMail<'a>> {
+        if part.subparts.is_empty() {
+            vec![part]
+        } else {
+            part.subparts
+                .iter()
+                .flat_map(Self::flatten_parts)
+                .collect::<Vec<_>>()
         }
     }
+
+    pub fn from_bytes(mime: &str, bytes: &[u8]) -> Result<Self> {
+        let msg = mailparse::parse_mail(bytes)?;
+        let (text_part, html_part, has_attachment) = Self::flatten_parts(&msg).into_iter().fold(
+            (None, None, false),
+            |(mut text_part, mut html_part, mut has_attachment), part| {
+                let ctype = part
+                    .get_headers()
+                    .get_first_value("content-type")
+                    .unwrap_or_default();
+
+                if text_part.is_none() && ctype.starts_with("text/plain") {
+                    text_part = part.get_body().ok();
+                } else {
+                    if html_part.is_none() && ctype.starts_with("text/html") {
+                        html_part = part.get_body().ok();
+                    } else {
+                        has_attachment = true
+                    };
+                };
+
+                (text_part, html_part, has_attachment)
+            },
+        );
+
+        let content = if mime == "text/plain" {
+            text_part.or(html_part).unwrap_or_default()
+        } else {
+            html_part.or(text_part).unwrap_or_default()
+        };
+
+        Ok(Self {
+            content,
+            has_attachment,
+        })
+    }
+}
+
+// Message
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Flag {
+    Seen,
+    Answered,
+    Flagged,
+}
+
+impl Flag {
+    fn from_imap_flag(flag: &imap::types::Flag<'_>) -> Option<Self> {
+        match flag {
+            imap::types::Flag::Seen => Some(Self::Seen),
+            imap::types::Flag::Answered => Some(Self::Answered),
+            imap::types::Flag::Flagged => Some(Self::Flagged),
+            _ => None,
+        }
+    }
+}
+
+#[derive(Debug, Serialize)]
+pub struct Msg {
+    pub uid: u32,
+    pub flags: Vec<Flag>,
+    pub subject: String,
+    pub sender: String,
+    pub date: String,
+
+    #[serde(skip_serializing)]
+    raw: Vec<u8>,
 }
 
 impl From<Vec<u8>> for Msg {
-    fn from(item: Vec<u8>) -> Self {
+    fn from(raw: Vec<u8>) -> Self {
         Self {
             uid: 0,
             flags: vec![],
-            raw: item,
+            subject: String::from(""),
+            sender: String::from(""),
+            date: String::from(""),
+            raw,
         }
     }
 }
 
+impl From<String> for Msg {
+    fn from(raw: String) -> Self {
+        Self::from(raw.as_bytes().to_vec())
+    }
+}
+
 impl From<&imap::types::Fetch> for Msg {
     fn from(fetch: &imap::types::Fetch) -> Self {
-        Self {
-            uid: fetch.uid.unwrap_or_default(),
-            flags: vec![],
-            raw: fetch.body().unwrap_or_default().to_vec(),
+        match fetch.envelope() {
+            None => Self::from(fetch.body().unwrap_or_default().to_vec()),
+            Some(envelope) => Self {
+                uid: fetch.uid.unwrap_or_default(),
+                flags: fetch
+                    .flags()
+                    .into_iter()
+                    .filter_map(Flag::from_imap_flag)
+                    .collect::<Vec<_>>(),
+                subject: envelope
+                    .subject
+                    .and_then(|subj| rfc2047_decoder::decode(subj).ok())
+                    .unwrap_or_default(),
+                sender: envelope
+                    .from
+                    .as_ref()
+                    .and_then(|addrs| addrs.first()?.name)
+                    .and_then(|name| rfc2047_decoder::decode(name).ok())
+                    .unwrap_or_default(),
+                date: fetch
+                    .internal_date()
+                    .map(|date| date.naive_local().to_string())
+                    .unwrap_or_default(),
+                raw: fetch.body().unwrap_or_default().to_vec(),
+            },
         }
     }
 }
@@ -174,43 +373,13 @@ impl<'a> Msg {
         Ok(text_bodies.join("\r\n"))
     }
 
-    fn extract_attachments_into(part: &mailparse::ParsedMail, parts: &mut Vec<(String, Vec<u8>)>) {
-        match part.subparts.len() {
-            0 => {
-                let content_disp = part.get_content_disposition();
-                let content_type = part
-                    .get_headers()
-                    .get_first_value("content-type")
-                    .unwrap_or_default();
-
-                let default_attachment_name = format!("attachment-{}", parts.len());
-                let attachment_name = content_disp
-                    .params
-                    .get("filename")
-                    .unwrap_or(&default_attachment_name)
-                    .to_owned();
-
-                if !content_type.starts_with("text") {
-                    parts.push((attachment_name, part.get_body_raw().unwrap_or_default()))
-                }
-            }
-            _ => {
-                part.subparts
-                    .iter()
-                    .for_each(|part| Self::extract_attachments_into(part, parts));
-            }
-        }
-    }
-
-    pub fn extract_attachments(&self) -> Result<Vec<(String, Vec<u8>)>> {
-        let mut parts = vec![];
-        Self::extract_attachments_into(&self.parse()?, &mut parts);
-        Ok(parts)
-    }
-
-    pub fn build_new_tpl(config: &Config, account: &Account) -> Result<String> {
+    pub fn build_new_tpl(config: &Config, account: &Account) -> Result<Tpl> {
         let mut tpl = vec![];
 
+        // "Content" headers
+        tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
+        tpl.push("Content-Transfer-Encoding: 8bit".to_string());
+
         // "From" header
         tpl.push(format!("From: {}", config.address(account)));
 
@@ -220,14 +389,18 @@ impl<'a> Msg {
         // "Subject" header
         tpl.push("Subject: ".to_string());
 
-        Ok(tpl.join("\r\n"))
+        Ok(Tpl(tpl.join("\r\n")))
     }
 
-    pub fn build_reply_tpl(&self, config: &Config, account: &Account) -> Result<String> {
+    pub fn build_reply_tpl(&self, config: &Config, account: &Account) -> Result<Tpl> {
         let msg = &self.parse()?;
         let headers = msg.get_headers();
         let mut tpl = vec![];
 
+        // "Content" headers
+        tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
+        tpl.push("Content-Transfer-Encoding: 8bit".to_string());
+
         // "From" header
         tpl.push(format!("From: {}", config.address(account)));
 
@@ -251,23 +424,27 @@ impl<'a> Msg {
         tpl.push(String::new());
 
         // Original msg prepend with ">"
-        let thread = msg
-            .get_body()
-            .unwrap()
-            .split("\r\n")
+        let thread = self
+            .text_bodies("text/plain")?
+            .replace("\r", "")
+            .split("\n")
             .map(|line| format!(">{}", line))
             .collect::<Vec<String>>()
             .join("\r\n");
         tpl.push(thread);
 
-        Ok(tpl.join("\r\n"))
+        Ok(Tpl(tpl.join("\r\n")))
     }
 
-    pub fn build_reply_all_tpl(&self, config: &Config, account: &Account) -> Result<String> {
+    pub fn build_reply_all_tpl(&self, config: &Config, account: &Account) -> Result<Tpl> {
         let msg = &self.parse()?;
         let headers = msg.get_headers();
         let mut tpl = vec![];
 
+        // "Content" headers
+        tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
+        tpl.push("Content-Transfer-Encoding: 8bit".to_string());
+
         // "From" header
         tpl.push(format!("From: {}", config.address(account)));
 
@@ -333,23 +510,26 @@ impl<'a> Msg {
         tpl.push(String::new());
 
         // Original msg prepend with ">"
-        let thread = msg
-            .get_body()
-            .unwrap()
+        let thread = self
+            .text_bodies("text/plain")?
             .split("\r\n")
             .map(|line| format!(">{}", line))
             .collect::<Vec<String>>()
             .join("\r\n");
         tpl.push(thread);
 
-        Ok(tpl.join("\r\n"))
+        Ok(Tpl(tpl.join("\r\n")))
     }
 
-    pub fn build_forward_tpl(&self, config: &Config, account: &Account) -> Result<String> {
+    pub fn build_forward_tpl(&self, config: &Config, account: &Account) -> Result<Tpl> {
         let msg = &self.parse()?;
         let headers = msg.get_headers();
         let mut tpl = vec![];
 
+        // "Content" headers
+        tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
+        tpl.push("Content-Transfer-Encoding: 8bit".to_string());
+
         // "From" header
         tpl.push(format!("From: {}", config.address(account)));
 
@@ -365,54 +545,36 @@ impl<'a> Msg {
 
         // Original msg
         tpl.push("-------- Forwarded Message --------".to_string());
-        tpl.push(msg.get_body().unwrap_or(String::new()));
+        tpl.push(self.text_bodies("text/plain")?);
 
-        Ok(tpl.join("\r\n"))
+        Ok(Tpl(tpl.join("\r\n")))
     }
 }
 
 impl DisplayRow for Msg {
     fn to_row(&self) -> Vec<table::Cell> {
-        match self.parse() {
-            Err(_) => vec![],
-            Ok(parsed) => {
-                let headers = parsed.get_headers();
-
-                let uid = &self.uid.to_string();
-                let flags = match self.extract_attachments().map(|vec| vec.is_empty()) {
-                    Ok(false) => "",
-                    _ => " ",
-                };
-                let sender = headers
-                    .get_first_value("reply-to")
-                    .or(headers.get_first_value("from"))
-                    .unwrap_or_default();
-                let subject = headers.get_first_value("subject").unwrap_or_default();
-                let date = headers.get_first_value("date").unwrap_or_default();
-
-                {
-                    use crate::table::*;
-
-                    vec![
-                        Cell::new(&[RED], &uid),
-                        Cell::new(&[WHITE], &flags),
-                        Cell::new(&[BLUE], &sender),
-                        FlexCell::new(&[GREEN], &subject),
-                        Cell::new(&[YELLOW], &date),
-                    ]
-                }
-            }
-        }
+        use crate::table::*;
+
+        vec![
+            Cell::new(&[RED], &self.uid.to_string()),
+            Cell::new(&[BLUE], &self.sender),
+            FlexCell::new(&[GREEN], &self.subject),
+            Cell::new(&[YELLOW], &self.date),
+        ]
     }
 }
 
-impl<'a> DisplayTable<'a, Msg> for Vec<Msg> {
+// Msgs
+
+#[derive(Debug, Serialize)]
+pub struct Msgs(pub Vec<Msg>);
+
+impl<'a> DisplayTable<'a, Msg> for Msgs {
     fn header_row() -> Vec<table::Cell> {
         use crate::table::*;
 
         vec![
             Cell::new(&[BOLD, UNDERLINE, WHITE], "UID"),
-            Cell::new(&[BOLD, UNDERLINE, WHITE], "FLAGS"),
             Cell::new(&[BOLD, UNDERLINE, WHITE], "SENDER"),
             FlexCell::new(&[BOLD, UNDERLINE, WHITE], "SUBJECT"),
             Cell::new(&[BOLD, UNDERLINE, WHITE], "DATE"),
@@ -420,6 +582,12 @@ impl<'a> DisplayTable<'a, Msg> for Vec<Msg> {
     }
 
     fn rows(&self) -> &Vec<Msg> {
-        self
+        &self.0
+    }
+}
+
+impl fmt::Display for Msgs {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{}", self.to_table())
     }
 }
diff --git a/src/output.rs b/src/output.rs
new file mode 100644
index 00000000..70636129
--- /dev/null
+++ b/src/output.rs
@@ -0,0 +1,71 @@
+use serde::Serialize;
+use std::{
+    fmt::{self, Display},
+    io,
+    process::Command,
+    result, string,
+};
+
+// Error wrapper
+
+#[derive(Debug)]
+pub enum Error {
+    IoError(io::Error),
+    ParseUtf8Error(string::FromUtf8Error),
+    SerializeJsonError(serde_json::Error),
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "input: ")?;
+
+        match self {
+            Error::IoError(err) => err.fmt(f),
+            Error::ParseUtf8Error(err) => err.fmt(f),
+            Error::SerializeJsonError(err) => err.fmt(f),
+        }
+    }
+}
+
+impl From<io::Error> for Error {
+    fn from(err: io::Error) -> Error {
+        Error::IoError(err)
+    }
+}
+
+impl From<string::FromUtf8Error> for Error {
+    fn from(err: string::FromUtf8Error) -> Error {
+        Error::ParseUtf8Error(err)
+    }
+}
+
+impl From<serde_json::Error> for Error {
+    fn from(err: serde_json::Error) -> Error {
+        Error::SerializeJsonError(err)
+    }
+}
+
+// Result wrapper
+
+type Result<T> = result::Result<T, Error>;
+
+// Utils
+
+pub fn run_cmd(cmd: &str) -> Result<String> {
+    let output = if cfg!(target_os = "windows") {
+        Command::new("cmd").args(&["/C", cmd]).output()?
+    } else {
+        Command::new("sh").arg("-c").arg(cmd).output()?
+    };
+
+    Ok(String::from_utf8(output.stdout)?)
+}
+
+pub fn print<T: Display + Serialize>(output_type: &str, item: T) -> Result<()> {
+    match output_type {
+        "json" => print!("{}", serde_json::to_string(&item)?),
+        "text" | _ => println!("{}", item.to_string()),
+    }
+
+    Ok(())
+}
diff --git a/src/smtp.rs b/src/smtp.rs
index fd4a338e..8da58a03 100644
--- a/src/smtp.rs
+++ b/src/smtp.rs
@@ -42,6 +42,9 @@ type Result<T> = result::Result<T, Error>;
 pub fn send(account: &Account, msg: &lettre::Message) -> Result<()> {
     use lettre::Transport;
 
+    // TODO
+    // lettre::transport::smtp::SmtpTransport::starttls_relay
+
     lettre::transport::smtp::SmtpTransport::relay(&account.smtp_host)?
         .credentials(account.smtp_creds()?)
         .build()
diff --git a/src/table.rs b/src/table.rs
index 5fce500e..6eb28a0a 100644
--- a/src/table.rs
+++ b/src/table.rs
@@ -60,15 +60,11 @@ impl Cell {
         let style_end = "\x1b[0m";
 
         if col_size > 0 && self.printable_value_len() > col_size {
-            let col_size = self
-                .value
-                .char_indices()
-                .map(|(i, _)| i)
-                .nth(col_size)
-                .unwrap()
-                - 2;
-
-            String::from(style_begin + &self.value[0..=col_size] + "… " + style_end)
+            let value: String = self.value.chars().collect::<Vec<_>>()[0..=col_size - 2]
+                .into_iter()
+                .collect();
+
+            String::from(style_begin + &value + "… " + style_end)
         } else {
             let padding = if col_size == 0 {
                 "".to_string()