mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 05:07:55 +08:00
143 lines
4.8 KiB
Rust
143 lines
4.8 KiB
Rust
// This file is part of Himalaya, a CLI to manage emails.
|
|
//
|
|
// Copyright (C) 2022-2026 soywod <pimalaya.org@posteo.net>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it under
|
|
// the terms of the GNU Affero General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
|
// later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
// details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
use std::{
|
|
collections::BTreeMap,
|
|
io::{stdin, BufRead, IsTerminal},
|
|
};
|
|
|
|
use anyhow::{bail, Result};
|
|
use clap::Parser;
|
|
use io_jmap::{
|
|
client::JmapClientStd,
|
|
rfc8621::{capabilities::MAIL, email::EmailImport},
|
|
};
|
|
use pimalaya_cli::printer::{Message, Printer};
|
|
use pimalaya_stream::tls::Tls;
|
|
use url::Url;
|
|
|
|
use crate::jmap::{
|
|
client::{jmap_http_auth, JmapClient},
|
|
error::format_set_error,
|
|
};
|
|
|
|
/// Import an RFC 5322 message into a mailbox (upload + Email/import).
|
|
///
|
|
/// Reads the raw message from stdin or as trailing arguments. Use
|
|
/// `--upload-only` to stop after the upload and print the blobId.
|
|
#[derive(Debug, Parser)]
|
|
pub struct JmapEmailImportCommand {
|
|
/// Mailbox ID(s) to place the imported email in.
|
|
#[arg(long, value_name = "MAILBOX-ID")]
|
|
pub mailbox_id: Vec<String>,
|
|
|
|
/// Keywords to set on the imported email (e.g. `$seen`).
|
|
#[arg(long, value_name = "KEYWORD")]
|
|
pub keyword: Vec<String>,
|
|
|
|
/// Override the `receivedAt` timestamp (RFC 3339).
|
|
#[arg(long, value_name = "DATE")]
|
|
pub received_at: Option<String>,
|
|
|
|
/// Only upload the blob and print the blobId; skip Email/import.
|
|
#[arg(long)]
|
|
pub upload_only: bool,
|
|
|
|
/// The raw RFC 5322 message (headers + body). Read from stdin if omitted.
|
|
#[arg(trailing_var_arg = true)]
|
|
#[arg(name = "message", value_name = "MESSAGE")]
|
|
pub message: Vec<String>,
|
|
}
|
|
|
|
impl JmapEmailImportCommand {
|
|
pub fn execute(self, printer: &mut impl Printer, mut client: JmapClient) -> Result<()> {
|
|
let data: Vec<u8> = if stdin().is_terminal() || printer.is_json() {
|
|
self.message
|
|
.join(" ")
|
|
.replace('\r', "")
|
|
.replace('\n', "\r\n")
|
|
.into_bytes()
|
|
} else {
|
|
let lines: Vec<String> = stdin().lock().lines().map_while(Result::ok).collect();
|
|
lines.join("\r\n").into_bytes()
|
|
};
|
|
|
|
let session = client.session().expect("session loaded by new_jmap_client");
|
|
let api_url = session.api_url.clone();
|
|
let account_id = session
|
|
.primary_accounts
|
|
.get(MAIL)
|
|
.map(|s| s.as_str())
|
|
.unwrap_or("");
|
|
let upload_url: Url = session
|
|
.upload_url
|
|
.replace("{accountId}", account_id)
|
|
.parse()?;
|
|
|
|
let blob_id = if same_authority(&api_url, &upload_url) {
|
|
client
|
|
.blob_upload(&upload_url, "message/rfc822", data)?
|
|
.blob_id
|
|
} else {
|
|
let mut tls: Tls = client.config.tls.clone().into();
|
|
tls.rustls.alpn = vec!["http/1.1".into()];
|
|
let http_auth = jmap_http_auth(client.config.auth.clone())?;
|
|
let mut upload_client = JmapClientStd::connect(&upload_url, &tls, http_auth)?;
|
|
upload_client
|
|
.blob_upload(&upload_url, "message/rfc822", data)?
|
|
.blob_id
|
|
};
|
|
|
|
if self.upload_only {
|
|
return printer.out(Message::new(blob_id));
|
|
}
|
|
|
|
let mailbox_ids: BTreeMap<String, bool> =
|
|
self.mailbox_id.into_iter().map(|m| (m, true)).collect();
|
|
|
|
let keywords = if self.keyword.is_empty() {
|
|
None
|
|
} else {
|
|
Some(self.keyword.iter().map(|kw| (kw.clone(), true)).collect())
|
|
};
|
|
|
|
let import = EmailImport {
|
|
blob_id: blob_id.clone(),
|
|
mailbox_ids,
|
|
keywords,
|
|
received_at: self.received_at,
|
|
};
|
|
|
|
let mut emails = BTreeMap::new();
|
|
emails.insert(blob_id.clone(), import);
|
|
|
|
let output = client.email_import(emails)?;
|
|
|
|
if let Some(err) = output.not_created.get(&blob_id) {
|
|
let mut msg = format!("Import JMAP email from blob `{blob_id}` error");
|
|
msg.push_str(&format_set_error(err));
|
|
bail!(msg);
|
|
}
|
|
|
|
printer.out(Message::new("Email successfully imported"))
|
|
}
|
|
}
|
|
|
|
fn same_authority(a: &Url, b: &Url) -> bool {
|
|
a.host() == b.host() && a.port_or_known_default() == b.port_or_known_default()
|
|
}
|