mirror of
https://github.com/rustmailer/bichon.git
synced 2026-08-03 07:48:34 +02:00
feat: support export account emails to a single mbox file
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
use bichon_core::{base64_encode, store::envelope::Envelope};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
use crate::BichonCtlConfig;
|
||||
|
||||
pub async fn download_and_export_with_json_header(
|
||||
client: &Client,
|
||||
config: &BichonCtlConfig,
|
||||
envelope: Envelope,
|
||||
file: &mut tokio::fs::File,
|
||||
) -> bool {
|
||||
let url = format!(
|
||||
"{}/api/v1/download-message/{}/{}",
|
||||
config.base_url, &envelope.account_id, &envelope.id
|
||||
);
|
||||
|
||||
let response = match client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", config.api_token))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(res) => {
|
||||
if !res.status().is_success() {
|
||||
eprintln!(
|
||||
" ✘ HTTP Error {}: Failed for {}",
|
||||
res.status(),
|
||||
&envelope.id
|
||||
);
|
||||
return false;
|
||||
}
|
||||
res
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ✘ Network error: {} for {}", e, &envelope.id);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let email_bytes = match response.bytes().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
" ✘ Failed to read response body for {}: {}",
|
||||
&envelope.id, e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let date_dt = Utc.timestamp_opt(envelope.date / 1000, 0).unwrap();
|
||||
let date_str = date_dt.format("%a %b %e %H:%M:%S %Y").to_string();
|
||||
let from_line = format!("From {} {}\n", envelope.from.clone(), date_str);
|
||||
|
||||
let custom_header = build_metadata_header(BichonMetadata {
|
||||
account_email: envelope.account_email,
|
||||
mailbox_name: envelope.mailbox_name,
|
||||
tags: envelope.tags,
|
||||
});
|
||||
|
||||
let mut final_buffer =
|
||||
Vec::with_capacity(from_line.len() + custom_header.len() + email_bytes.len() + 2);
|
||||
final_buffer.extend_from_slice(from_line.as_bytes());
|
||||
final_buffer.extend_from_slice(custom_header.as_bytes());
|
||||
final_buffer.extend_from_slice(&email_bytes);
|
||||
final_buffer.extend_from_slice(b"\n\n");
|
||||
|
||||
if let Err(e) = file.write_all(&final_buffer).await {
|
||||
eprintln!(" ✘ IO Error: Failed to write to mbox: {}", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct BichonMetadata {
|
||||
pub account_email: Option<String>,
|
||||
pub mailbox_name: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
fn build_metadata_header(meta: BichonMetadata) -> String {
|
||||
let json_str = serde_json::to_string(&meta).ok().unwrap();
|
||||
let encoded = base64_encode!(json_str);
|
||||
format!("X-Bichon-Metadata: {}\r\n", encoded)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod download;
|
||||
pub mod search;
|
||||
pub mod sender;
|
||||
pub mod stats;
|
||||
@@ -0,0 +1,54 @@
|
||||
use bichon_core::{
|
||||
common::paginated::DataPage,
|
||||
message::search::{EmailSearchFilter, EmailSearchRequest, SortBy},
|
||||
store::envelope::Envelope,
|
||||
};
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::BichonCtlConfig;
|
||||
|
||||
pub async fn search_messages(
|
||||
client: &Client,
|
||||
config: &BichonCtlConfig,
|
||||
page: u64,
|
||||
page_size: u64,
|
||||
) -> Option<DataPage<Envelope>> {
|
||||
let url = format!("{}/api/v1/search-messages", config.base_url);
|
||||
|
||||
let payload = EmailSearchRequest {
|
||||
filter: EmailSearchFilter::default(),
|
||||
page,
|
||||
page_size,
|
||||
sort_by: Some(SortBy::DATE),
|
||||
desc: Some(false),
|
||||
};
|
||||
|
||||
match client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", config.api_token))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(res) if res.status().is_success() => match res.json::<DataPage<Envelope>>().await {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
eprintln!(" ✘ Failed to parse search response: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
let error_body = res.text().await.unwrap_or_default();
|
||||
eprintln!(
|
||||
" ✘ Failed to search messages. Status: {}\n Server error: {}",
|
||||
status, error_body
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ✘ Network error performing search: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
|
||||
//
|
||||
// This file is part of the Bichon Email Archiving Project
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use console::style;
|
||||
use reqwest::Client;
|
||||
|
||||
use bichon_core::import::BatchEmlRequest;
|
||||
|
||||
use crate::BichonCtlConfig;
|
||||
|
||||
pub async fn send_batch_request(
|
||||
client: &Client,
|
||||
config: &BichonCtlConfig,
|
||||
account_id: u64,
|
||||
folder: &str,
|
||||
emls: Vec<String>,
|
||||
) {
|
||||
let url = format!("{}/api/v1/import", config.base_url);
|
||||
let payload = BatchEmlRequest {
|
||||
account_id,
|
||||
mail_folder: folder.to_string(),
|
||||
emls,
|
||||
};
|
||||
|
||||
let count = payload.emls.len();
|
||||
|
||||
match client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", config.api_token))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(res) if res.status().is_success() => {
|
||||
println!(
|
||||
" {} Sent {} emails to [{}]",
|
||||
style("✔").green(),
|
||||
count,
|
||||
folder
|
||||
);
|
||||
}
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
let error_body = res.text().await.unwrap_or_default();
|
||||
eprintln!(
|
||||
" {} Failed to send to [{}]. Status: {}\n Server error: {}",
|
||||
style("✘").red(),
|
||||
folder,
|
||||
status,
|
||||
error_body
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
" {} Network error on [{}]: {}",
|
||||
style("✘").red(),
|
||||
folder,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use bichon_core::account::stats::AccountStats;
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::BichonCtlConfig;
|
||||
|
||||
pub async fn fetch_account_stats(
|
||||
client: &Client,
|
||||
config: &BichonCtlConfig,
|
||||
account_id: u64,
|
||||
) -> Option<AccountStats> {
|
||||
let url = format!("{}/api/v1/accounts/{}/stats", config.base_url, account_id);
|
||||
|
||||
match client
|
||||
.get(&url)
|
||||
.header("Authorization", format!("Bearer {}", config.api_token))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(res) if res.status().is_success() => {
|
||||
match res.json::<AccountStats>().await {
|
||||
Ok(stats) => Some(stats),
|
||||
Err(e) => {
|
||||
eprintln!(" ✘ Failed to parse stats response: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
let error_body = res.text().await.unwrap_or_default();
|
||||
eprintln!(
|
||||
" ✘ Failed to fetch stats for account [{}]. Status: {}\n Server error: {}",
|
||||
account_id,
|
||||
status,
|
||||
error_body
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
" ✘ Network error fetching stats for [{}]: {}",
|
||||
account_id,
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user