mirror of
https://github.com/chatmail/core.git
synced 2026-04-27 18:36:30 +03:00
* Copypaste-merge my old work * Start implementing mailinglists the new way * Create pseudo contact * Fine-tune docs * Remove some unnecessary changes * style * Make a stock str * Fix a crash. Yes, this line caused a panic when reconfiguring on Android (without a reasonable error log). Also, I could not receive any messages anymore. * rfmt * Add tests and make them pass * Even more tests * rfmt * Enhance test and fix bug * Don't update the sender name when prefetching because maybe it's a mailing list * Use an enum instead of numbers for the decision * Don't remove anyone from mailing lists * Fix bug in the regex * Adjust error msg * Compile error after rebase * Correctly emit event * Add dc_msg_is_mailing_list so that you can find out whether messages in the deaddrop belong to mailing lists. * Add received headers to unit tests * Comments, small tweaks * Use dc_msg_get_override_sender_name instead of dc_msg_get_sender_name * Add dc_msg_get_sender_first_name() because sometimes the first name was not correctly shown in mailing lists * small fixes, don't let the user modify mailing list groups * Hide contacts for addresses like noreply@github.com and reply+AEJ...@reply.github.com When testing mailing lists, I noticed that sometimes a mailing list contact got a name (like, hocuri <noreply@github.com>). It turned out that ages ago, I had accidentally written an email to - in this example - hocuri <noreply@github.com> and it had been added to the contacts list. This hides email addresses from the contacts list that are obviously not meant to be written at and prevents updating the names. * Comment, clippy * Replace u32 with ChatId * Resolve lost of small issues from the reviews * remove dc_msg_get_sender_first_name * add dc_msg_get_real_chat_id() this allows to check if a contact request belongs to a mailing list and to show name/avatar/whatever of the mailinglist. another approach was to duplicate some chat-apis for messages (eg. dc_msg_is_mailing_list()) however that would require far more new apis. the idea to change the behavior of dc_msg_get_chat_id() would be more clean, however, that easily breaks existing implementations and can led to hard to find bugs. * remove now unused Message.is_mailing_list() * if a name for a mailing list is missing, use List-ID * fix comment * fix error message * document how dc_get_chat_contacts() works for mailing lists * refine decide api (#2185) * add DC_DECIDE* constants to deltachat.h, tweak documentation * use StartChat/Block/NotNow instead of Yes/Never/NotNow * decide_on_contact_request works on ctx/msg-id functions working on message-objects usually do not read or write directly on the database. therefore, decide_on_contact_request() should not work with message-objects as well, it is even a bit misleading, as eg. chat-id of the object is not modified. instead, the function works on context, similar to dc_send_msg(), dc_create_chat() and so on. for now, i moved it to context, could maybe be part od MsgId. * Update src/chatlist.rs Co-authored-by: Hocuri <hocuri@gmx.de> Co-authored-by: Hocuri <hocuri@gmx.de> * refine documentation * re-add accidentally deleted Param::MailingList * remove pseudo-contact in domain @mailing.list 1. the pseudo-contact was added to the mailing list contacts, which is not needed. might be that we want to add the recent contacts there in a subsequent pr (we do not know all contacts of a mailing list) 2. the pseudo-contact was used to block mailing lists; this is done by setting the chat to Blocked::Manually now 3- the pseudo-contact was used for unblocking; as it is very neat not to require additional ui for mailing list unblocking, might be that we introduce a similar pseudo-contact for this limited purpose in a subsequent pr, however, the pseudo-contact needs to exist only during unblocking then, maybe also the special domain is not needed, we'll see :) * Move dc_decide_on_contact_request() up to the dc_context section as it's a member of dc_context now More specifically, to the "handle messages" section * re-introduce Chattype::Mailinglist (#2195) * re-introduce Chattype::Mailinglist * exhaustive chattype-check in fetch_existing_msgs() and get_summary2() * exhaustive chattype-check in ndn_maybe_add_info_msg() * exhaustive chattype-check in message::fill() * remove dc_chat_is_mailing_list() from ffi Co-authored-by: B. Petersen <r10s@b44t.com>
622 lines
22 KiB
Rust
622 lines
22 KiB
Rust
//! Utilities to help writing tests.
|
|
//!
|
|
//! This private module is only compiled for test runs.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::fmt;
|
|
use std::ops::Deref;
|
|
use std::str::FromStr;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use ansi_term::Color;
|
|
use async_std::future::Future;
|
|
use async_std::path::PathBuf;
|
|
use async_std::pin::Pin;
|
|
use async_std::sync::{Arc, RwLock};
|
|
use chat::ChatItem;
|
|
use once_cell::sync::Lazy;
|
|
use tempfile::{tempdir, TempDir};
|
|
|
|
use crate::chat::{self, Chat, ChatId};
|
|
use crate::chatlist::Chatlist;
|
|
use crate::config::Config;
|
|
use crate::constants::{Viewtype, DC_CONTACT_ID_SELF, DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
|
use crate::contact::{Contact, Origin};
|
|
use crate::context::Context;
|
|
use crate::dc_receive_imf::dc_receive_imf;
|
|
use crate::dc_tools::EmailAddress;
|
|
use crate::events::{Event, EventType};
|
|
use crate::job::Action;
|
|
use crate::key::{self, DcKey};
|
|
use crate::message::{update_msg_state, Message, MessageState, MsgId};
|
|
use crate::mimeparser::MimeMessage;
|
|
use crate::param::{Param, Params};
|
|
|
|
type EventSink =
|
|
dyn Fn(Event) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> + Send + Sync + 'static;
|
|
|
|
/// Map of [`Context::id`] to names for [`TestContext`]s.
|
|
static CONTEXT_NAMES: Lazy<std::sync::RwLock<BTreeMap<u32, String>>> =
|
|
Lazy::new(|| std::sync::RwLock::new(BTreeMap::new()));
|
|
|
|
/// A Context and temporary directory.
|
|
///
|
|
/// The temporary directory can be used to store the SQLite database,
|
|
/// see e.g. [test_context] which does this.
|
|
pub(crate) struct TestContext {
|
|
pub ctx: Context,
|
|
pub dir: TempDir,
|
|
/// Counter for fake IMAP UIDs in [recv_msg], for private use in that function only.
|
|
recv_idx: RwLock<u32>,
|
|
/// Functions to call for events received.
|
|
event_sinks: Arc<RwLock<Vec<Box<EventSink>>>>,
|
|
}
|
|
|
|
impl fmt::Debug for TestContext {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.debug_struct("TestContext")
|
|
.field("ctx", &self.ctx)
|
|
.field("dir", &self.dir)
|
|
.field("recv_idx", &self.recv_idx)
|
|
.field("event_sinks", &String::from("Vec<EventSink>"))
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
impl TestContext {
|
|
/// Creates a new [`TestContext`].
|
|
///
|
|
/// The [Context] will be created and have an SQLite database named "db.sqlite" in the
|
|
/// [TestContext.dir] directory. This directory is cleaned up when the [TestContext] is
|
|
/// dropped.
|
|
///
|
|
/// [Context]: crate::context::Context
|
|
pub async fn new() -> Self {
|
|
Self::new_named(None).await
|
|
}
|
|
|
|
/// Creates a new [`TestContext`] with a set name used in event logging.
|
|
pub async fn with_name(name: impl Into<String>) -> Self {
|
|
Self::new_named(Some(name.into())).await
|
|
}
|
|
|
|
async fn new_named(name: Option<String>) -> Self {
|
|
use rand::Rng;
|
|
|
|
let dir = tempdir().unwrap();
|
|
let dbfile = dir.path().join("db.sqlite");
|
|
let id = rand::thread_rng().gen();
|
|
if let Some(name) = name {
|
|
let mut context_names = CONTEXT_NAMES.write().unwrap();
|
|
context_names.insert(id, name);
|
|
}
|
|
let ctx = Context::new("FakeOS".into(), dbfile.into(), id)
|
|
.await
|
|
.unwrap();
|
|
|
|
let events = ctx.get_event_emitter();
|
|
let event_sinks: Arc<RwLock<Vec<Box<EventSink>>>> = Arc::new(RwLock::new(Vec::new()));
|
|
let sinks = Arc::clone(&event_sinks);
|
|
async_std::task::spawn(async move {
|
|
while let Some(event) = events.recv().await {
|
|
{
|
|
let sinks = sinks.read().await;
|
|
for sink in sinks.iter() {
|
|
sink(event.clone()).await;
|
|
}
|
|
}
|
|
receive_event(event);
|
|
}
|
|
});
|
|
|
|
Self {
|
|
ctx,
|
|
dir,
|
|
recv_idx: RwLock::new(0),
|
|
event_sinks,
|
|
}
|
|
}
|
|
|
|
/// Creates a new configured [`TestContext`].
|
|
///
|
|
/// This is a shortcut which automatically calls [`TestContext::configure_alice`] after
|
|
/// creating the context.
|
|
pub async fn new_alice() -> Self {
|
|
let t = Self::with_name("alice").await;
|
|
t.configure_alice().await;
|
|
t
|
|
}
|
|
|
|
/// Creates a new configured [`TestContext`].
|
|
///
|
|
/// This is a shortcut which configures bob@example.net with a fixed key.
|
|
pub async fn new_bob() -> Self {
|
|
let t = Self::with_name("bob").await;
|
|
let keypair = bob_keypair();
|
|
t.configure_addr(&keypair.addr.to_string()).await;
|
|
key::store_self_keypair(&t, &keypair, key::KeyPairUse::Default)
|
|
.await
|
|
.expect("Failed to save Bob's key");
|
|
t
|
|
}
|
|
|
|
/// Sets a name for this [`TestContext`] if one isn't yet set.
|
|
///
|
|
/// This will show up in events logged in the test output.
|
|
pub fn set_name(&self, name: impl Into<String>) {
|
|
let mut context_names = CONTEXT_NAMES.write().unwrap();
|
|
context_names
|
|
.entry(self.ctx.get_id())
|
|
.or_insert_with(|| name.into());
|
|
}
|
|
|
|
/// Add a new callback which will receive events.
|
|
///
|
|
/// The test context runs an async task receiving all events from the [`Context`], which
|
|
/// are logged to stdout. This allows you to register additional callbacks which will
|
|
/// receive all events in case your tests need to watch for a specific event.
|
|
pub async fn add_event_sink<F, R>(&self, sink: F)
|
|
where
|
|
// Aka `F: EventSink` but type aliases are not allowed.
|
|
F: Fn(Event) -> R + Send + Sync + 'static,
|
|
R: Future<Output = ()> + Send + 'static,
|
|
{
|
|
let mut sinks = self.event_sinks.write().await;
|
|
sinks.push(Box::new(move |evt| Box::pin(sink(evt))));
|
|
}
|
|
|
|
/// Configure with alice@example.com.
|
|
///
|
|
/// The context will be fake-configured as the alice user, with a pre-generated secret
|
|
/// key. The email address of the user is returned as a string.
|
|
pub async fn configure_alice(&self) -> String {
|
|
let keypair = alice_keypair();
|
|
self.configure_addr(&keypair.addr.to_string()).await;
|
|
key::store_self_keypair(&self.ctx, &keypair, key::KeyPairUse::Default)
|
|
.await
|
|
.expect("Failed to save Alice's key");
|
|
keypair.addr.to_string()
|
|
}
|
|
|
|
/// Configure as a given email address.
|
|
///
|
|
/// The context will be configured but the key will not be pre-generated so if a key is
|
|
/// used the fingerprint will be different every time.
|
|
pub async fn configure_addr(&self, addr: &str) {
|
|
self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
|
|
self.ctx
|
|
.set_config(Config::ConfiguredAddr, Some(addr))
|
|
.await
|
|
.unwrap();
|
|
self.ctx
|
|
.set_config(Config::Configured, Some("1"))
|
|
.await
|
|
.unwrap();
|
|
if let Some(name) = addr.split('@').next() {
|
|
self.set_name(name);
|
|
}
|
|
}
|
|
|
|
/// Retrieves a sent message from the jobs table.
|
|
///
|
|
/// This retrieves and removes a message which has been scheduled to send from the jobs
|
|
/// table. Messages are returned in the order they have been sent.
|
|
///
|
|
/// Panics if there is no message or on any error.
|
|
pub async fn pop_sent_msg(&self) -> SentMessage {
|
|
let start = Instant::now();
|
|
let (rowid, foreign_id, raw_params) = loop {
|
|
let row = self
|
|
.ctx
|
|
.sql
|
|
.query_row(
|
|
r#"
|
|
SELECT id, foreign_id, param
|
|
FROM jobs
|
|
WHERE action=?
|
|
ORDER BY desired_timestamp DESC;
|
|
"#,
|
|
paramsv![Action::SendMsgToSmtp],
|
|
|row| {
|
|
let id: i64 = row.get(0)?;
|
|
let foreign_id: i64 = row.get(1)?;
|
|
let param: String = row.get(2)?;
|
|
Ok((id, foreign_id, param))
|
|
},
|
|
)
|
|
.await;
|
|
if let Ok(row) = row {
|
|
break row;
|
|
}
|
|
if start.elapsed() < Duration::from_secs(3) {
|
|
async_std::task::sleep(Duration::from_millis(100)).await;
|
|
} else {
|
|
panic!("no sent message found in jobs table");
|
|
}
|
|
};
|
|
let id = MsgId::new(foreign_id as u32);
|
|
let params = Params::from_str(&raw_params).unwrap();
|
|
let blob_path = params
|
|
.get_blob(Param::File, &self.ctx, false)
|
|
.await
|
|
.expect("failed to parse blob from param")
|
|
.expect("no Param::File found in Params")
|
|
.to_abs_path();
|
|
self.ctx
|
|
.sql
|
|
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
|
|
.await
|
|
.expect("failed to remove job");
|
|
update_msg_state(&self.ctx, id, MessageState::OutDelivered).await;
|
|
SentMessage {
|
|
params,
|
|
blob_path,
|
|
sender_msg_id: id,
|
|
}
|
|
}
|
|
|
|
/// Parses a message.
|
|
///
|
|
/// Parsing a message does not run the entire receive pipeline, but is not without
|
|
/// side-effects either. E.g. if the message includes autocrypt headers the relevant
|
|
/// peerstates will be updated. Later receiving the message using [recv_msg] is
|
|
/// unlikely to be affected as the peerstate would be processed again in exactly the
|
|
/// same way.
|
|
pub async fn parse_msg(&self, msg: &SentMessage) -> MimeMessage {
|
|
MimeMessage::from_bytes(&self.ctx, msg.payload().as_bytes())
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
/// Receive a message.
|
|
///
|
|
/// Receives a message using the `dc_receive_imf()` pipeline.
|
|
pub async fn recv_msg(&self, msg: &SentMessage) {
|
|
let mut idx = self.recv_idx.write().await;
|
|
*idx += 1;
|
|
let received_msg =
|
|
"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n"
|
|
.to_owned()
|
|
+ &msg.payload();
|
|
dc_receive_imf(&self.ctx, received_msg.as_bytes(), "INBOX", *idx, false)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// Gets the most recent message of a chat.
|
|
///
|
|
/// Panics on errors or if the most recent message is a marker.
|
|
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message {
|
|
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None).await;
|
|
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
|
msg_id
|
|
} else {
|
|
panic!("Wrong item type");
|
|
};
|
|
Message::load_from_db(&self.ctx, *msg_id).await.unwrap()
|
|
}
|
|
|
|
/// Gets the most recent message over all chats.
|
|
pub async fn get_last_msg(&self) -> Message {
|
|
let chats = Chatlist::try_load(&self.ctx, 0, None, None).await.unwrap();
|
|
let msg_id = chats.get_msg_id(chats.len() - 1).unwrap();
|
|
Message::load_from_db(&self.ctx, msg_id).await.unwrap()
|
|
}
|
|
|
|
/// Creates or returns an existing 1:1 [`Chat`] with another account.
|
|
///
|
|
/// This first creates a contact using the configured details on the other account, then
|
|
/// creates a 1:1 chat with this contact.
|
|
pub async fn create_chat(&self, other: &TestContext) -> Chat {
|
|
let (contact_id, _modified) = Contact::add_or_lookup(
|
|
self,
|
|
other
|
|
.ctx
|
|
.get_config(Config::Displayname)
|
|
.await
|
|
.unwrap_or_default(),
|
|
other.ctx.get_config(Config::ConfiguredAddr).await.unwrap(),
|
|
Origin::ManuallyCreated,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let chat_id = chat::create_by_contact_id(self, contact_id).await.unwrap();
|
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
|
}
|
|
|
|
/// Creates or returns an existing [`Contact`] and 1:1 [`Chat`] with another email.
|
|
///
|
|
/// This first creates a contact from the `name` and `addr` and then creates a 1:1 chat
|
|
/// with this contact.
|
|
pub async fn create_chat_with_contact(&self, name: &str, addr: &str) -> Chat {
|
|
let contact = Contact::create(self, name, addr)
|
|
.await
|
|
.expect("failed to create contact");
|
|
let chat_id = chat::create_by_contact_id(self, contact).await.unwrap();
|
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
|
}
|
|
|
|
/// Retrieves the "self" chat.
|
|
pub async fn get_self_chat(&self) -> Chat {
|
|
let chat_id = chat::create_by_contact_id(self, DC_CONTACT_ID_SELF)
|
|
.await
|
|
.unwrap();
|
|
Chat::load_from_db(self, chat_id).await.unwrap()
|
|
}
|
|
|
|
/// Sends out the text message.
|
|
///
|
|
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
|
|
/// [`TestContext::recv_msg`] with the returned [`SentMessage`] if it wants to receive
|
|
/// the message.
|
|
pub async fn send_text(&self, chat_id: ChatId, txt: &str) -> SentMessage {
|
|
let mut msg = Message::new(Viewtype::Text);
|
|
msg.set_text(Some(txt.to_string()));
|
|
chat::prepare_msg(&self, chat_id, &mut msg).await.unwrap();
|
|
chat::send_msg(&self, chat_id, &mut msg).await.unwrap();
|
|
self.pop_sent_msg().await
|
|
}
|
|
|
|
/// Prints out the entire chat to stdout.
|
|
///
|
|
/// You can use this to debug your test by printing the entire chat conversation.
|
|
// This code is mainly the same as `log_msglist` in `cmdline.rs`, so one day, we could
|
|
// merge them to a public function in the `deltachat` crate.
|
|
#[allow(dead_code)]
|
|
pub async fn print_chat(&self, chat: &Chat) {
|
|
let msglist = chat::get_chat_msgs(&self, chat.get_id(), 0x1, None).await;
|
|
let msglist: Vec<MsgId> = msglist
|
|
.into_iter()
|
|
.map(|x| match x {
|
|
ChatItem::Message { msg_id } => msg_id,
|
|
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
|
|
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
|
})
|
|
.collect();
|
|
|
|
let mut lines_out = 0;
|
|
for msg_id in msglist {
|
|
if msg_id == MsgId::new(DC_MSG_ID_DAYMARKER) {
|
|
println!(
|
|
"--------------------------------------------------------------------------------"
|
|
);
|
|
|
|
lines_out += 1
|
|
} else if !msg_id.is_special() {
|
|
if lines_out == 0 {
|
|
println!(
|
|
"--------------------------------------------------------------------------------",
|
|
);
|
|
lines_out += 1
|
|
}
|
|
let msg = Message::load_from_db(&self, msg_id).await.unwrap();
|
|
log_msg(self, "", &msg).await;
|
|
}
|
|
}
|
|
if lines_out > 0 {
|
|
println!(
|
|
"--------------------------------------------------------------------------------"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Deref for TestContext {
|
|
type Target = Context;
|
|
|
|
fn deref(&self) -> &Context {
|
|
&self.ctx
|
|
}
|
|
}
|
|
|
|
/// A raw message as it was scheduled to be sent.
|
|
///
|
|
/// This is a raw message, probably in the shape DC was planning to send it but not having
|
|
/// passed through a SMTP-IMAP pipeline.
|
|
#[derive(Debug, Clone)]
|
|
pub struct SentMessage {
|
|
params: Params,
|
|
blob_path: PathBuf,
|
|
pub sender_msg_id: MsgId,
|
|
}
|
|
|
|
impl SentMessage {
|
|
/// A recipient the message was destined for.
|
|
///
|
|
/// If there are multiple recipients this is just a random one, so is not very useful.
|
|
pub fn recipient(&self) -> EmailAddress {
|
|
let raw = self
|
|
.params
|
|
.get(Param::Recipients)
|
|
.expect("no recipients in params");
|
|
let rcpt = raw.split(' ').next().expect("no recipient found");
|
|
rcpt.parse().expect("failed to parse email address")
|
|
}
|
|
|
|
/// The raw message payload.
|
|
pub fn payload(&self) -> String {
|
|
std::fs::read_to_string(&self.blob_path).unwrap()
|
|
}
|
|
}
|
|
|
|
/// Load a pre-generated keypair for alice@example.com from disk.
|
|
///
|
|
/// This saves CPU cycles by avoiding having to generate a key.
|
|
///
|
|
/// The keypair was created using the crate::key::tests::gen_key test.
|
|
pub fn alice_keypair() -> key::KeyPair {
|
|
let addr = EmailAddress::new("alice@example.com").unwrap();
|
|
let public =
|
|
key::SignedPublicKey::from_base64(include_str!("../test-data/key/alice-public.asc"))
|
|
.unwrap();
|
|
let secret =
|
|
key::SignedSecretKey::from_base64(include_str!("../test-data/key/alice-secret.asc"))
|
|
.unwrap();
|
|
key::KeyPair {
|
|
addr,
|
|
public,
|
|
secret,
|
|
}
|
|
}
|
|
|
|
/// Load a pre-generated keypair for bob@example.net from disk.
|
|
///
|
|
/// Like [alice_keypair] but a different key and identity.
|
|
pub fn bob_keypair() -> key::KeyPair {
|
|
let addr = EmailAddress::new("bob@example.net").unwrap();
|
|
let public =
|
|
key::SignedPublicKey::from_base64(include_str!("../test-data/key/bob-public.asc")).unwrap();
|
|
let secret =
|
|
key::SignedSecretKey::from_base64(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
|
key::KeyPair {
|
|
addr,
|
|
public,
|
|
secret,
|
|
}
|
|
}
|
|
|
|
/// Gets a specific message from a chat and asserts that the chat has a specific length.
|
|
///
|
|
/// Panics if the length of the chat is not `asserted_msgs_count` or if the chat item at `index` is not a Message.
|
|
#[allow(clippy::indexing_slicing)]
|
|
pub(crate) async fn get_chat_msg(
|
|
t: &TestContext,
|
|
chat_id: ChatId,
|
|
index: usize,
|
|
asserted_msgs_count: usize,
|
|
) -> Message {
|
|
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await;
|
|
assert_eq!(msgs.len(), asserted_msgs_count);
|
|
let msg_id = if let ChatItem::Message { msg_id } = msgs[index] {
|
|
msg_id
|
|
} else {
|
|
panic!("Wrong item type");
|
|
};
|
|
Message::load_from_db(&t.ctx, msg_id).await.unwrap()
|
|
}
|
|
|
|
/// Pretty-print an event to stdout
|
|
///
|
|
/// Done during tests this is captured by `cargo test` and associated with the test itself.
|
|
fn receive_event(event: Event) {
|
|
let green = Color::Green.normal();
|
|
let yellow = Color::Yellow.normal();
|
|
let red = Color::Red.normal();
|
|
|
|
let msg = match event.typ {
|
|
EventType::Info(msg) => format!("INFO: {}", msg),
|
|
EventType::SmtpConnected(msg) => format!("[SMTP_CONNECTED] {}", msg),
|
|
EventType::ImapConnected(msg) => format!("[IMAP_CONNECTED] {}", msg),
|
|
EventType::SmtpMessageSent(msg) => format!("[SMTP_MESSAGE_SENT] {}", msg),
|
|
EventType::Warning(msg) => format!("WARN: {}", yellow.paint(msg)),
|
|
EventType::Error(msg) => format!("ERROR: {}", red.paint(msg)),
|
|
EventType::ErrorNetwork(msg) => format!("{}", red.paint(format!("[NETWORK] msg={}", msg))),
|
|
EventType::ErrorSelfNotInGroup(msg) => {
|
|
format!("{}", red.paint(format!("[SELF_NOT_IN_GROUP] {}", msg)))
|
|
}
|
|
EventType::MsgsChanged { chat_id, msg_id } => format!(
|
|
"{}",
|
|
green.paint(format!(
|
|
"Received MSGS_CHANGED(chat_id={}, msg_id={})",
|
|
chat_id, msg_id,
|
|
))
|
|
),
|
|
EventType::ContactsChanged(_) => format!("{}", green.paint("Received CONTACTS_CHANGED()")),
|
|
EventType::LocationChanged(contact) => format!(
|
|
"{}",
|
|
green.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
|
|
),
|
|
EventType::ConfigureProgress { progress, comment } => {
|
|
if let Some(comment) = comment {
|
|
format!(
|
|
"{}",
|
|
green.paint(format!(
|
|
"Received CONFIGURE_PROGRESS({} ‰, {})",
|
|
progress, comment
|
|
))
|
|
)
|
|
} else {
|
|
format!(
|
|
"{}",
|
|
green.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
|
)
|
|
}
|
|
}
|
|
EventType::ImexProgress(progress) => format!(
|
|
"{}",
|
|
green.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
|
|
),
|
|
EventType::ImexFileWritten(file) => format!(
|
|
"{}",
|
|
green.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
|
|
),
|
|
EventType::ChatModified(chat) => format!(
|
|
"{}",
|
|
green.paint(format!("Received CHAT_MODIFIED({})", chat))
|
|
),
|
|
_ => format!("Received {:?}", event),
|
|
};
|
|
let context_names = CONTEXT_NAMES.read().unwrap();
|
|
match context_names.get(&event.id) {
|
|
Some(ref name) => println!("{} {}", name, msg),
|
|
None => println!("{} {}", event.id, msg),
|
|
}
|
|
}
|
|
|
|
/// Logs and individual message to stdout.
|
|
///
|
|
/// This includes a bunch of the message meta-data as well.
|
|
async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|
let contact = Contact::get_by_id(context, msg.get_from_id())
|
|
.await
|
|
.expect("invalid contact");
|
|
|
|
let contact_name = contact.get_name();
|
|
let contact_id = contact.get_id();
|
|
|
|
let statestr = match msg.get_state() {
|
|
MessageState::OutPending => " o",
|
|
MessageState::OutDelivered => " √",
|
|
MessageState::OutMdnRcvd => " √√",
|
|
MessageState::OutFailed => " !!",
|
|
_ => "",
|
|
};
|
|
let msgtext = msg.get_text();
|
|
println!(
|
|
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}",
|
|
prefix.as_ref(),
|
|
msg.get_id(),
|
|
if msg.get_showpadlock() { "🔒" } else { "" },
|
|
if msg.has_location() { "📍" } else { "" },
|
|
&contact_name,
|
|
contact_id,
|
|
msgtext.unwrap_or_default(),
|
|
if msg.get_from_id() == 1u32 {
|
|
""
|
|
} else if msg.get_state() == MessageState::InSeen {
|
|
"[SEEN]"
|
|
} else if msg.get_state() == MessageState::InNoticed {
|
|
"[NOTICED]"
|
|
} else {
|
|
"[FRESH]"
|
|
},
|
|
if msg.is_info() { "[INFO]" } else { "" },
|
|
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
|
format!(
|
|
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
|
msg.get_videochat_url().unwrap_or_default(),
|
|
msg.get_videochat_type().unwrap_or_default()
|
|
)
|
|
} else {
|
|
"".to_string()
|
|
},
|
|
if msg.is_forwarded() {
|
|
"[FORWARDED]"
|
|
} else {
|
|
""
|
|
},
|
|
statestr,
|
|
);
|
|
}
|