Author:
Hash:
Timestamp:
+84 -35 +/-9 browse
Kevin Schoon [me@kevinschoon.com]
996865c3b0a42da81d9306912fe2456e4d6fa3bf
Sun, 01 Dec 2024 17:29:14 +0000 (11 months ago)
| 1 | diff --git a/ayllu-mail/src/delivery.rs b/ayllu-mail/src/delivery.rs |
| 2 | index 5a58aba..a9e2f70 100644 |
| 3 | --- a/ayllu-mail/src/delivery.rs |
| 4 | +++ b/ayllu-mail/src/delivery.rs |
| 5 | @@ -1,6 +1,6 @@ |
| 6 | - use std::collections::HashMap; |
| 7 | + use std::{collections::HashMap, fmt::Display}; |
| 8 | |
| 9 | - use mail_parser::{Address, MessageParser}; |
| 10 | + use mail_parser::{Address, HeaderValue, Message, MessageParser}; |
| 11 | use maildir::Maildir; |
| 12 | |
| 13 | use ayllu_database::mail::MailExt; |
| 14 | @@ -25,6 +25,40 @@ fn addresses(address: Option<&Address<'_>>) -> Vec<String> { |
| 15 | } |
| 16 | } |
| 17 | |
| 18 | + /// Return all references and in-reply-to |
| 19 | + pub struct References(Option<String>, Vec<String>); |
| 20 | + |
| 21 | + impl From<Message<'_>> for References { |
| 22 | + fn from(value: Message<'_>) -> Self { |
| 23 | + let references = match value.references() { |
| 24 | + HeaderValue::Text(cow) => vec![cow.to_string()], |
| 25 | + HeaderValue::TextList(vec) => vec.iter().map(|text| text.to_string()).collect(), |
| 26 | + _ => vec![], |
| 27 | + }; |
| 28 | + References( |
| 29 | + value |
| 30 | + .in_reply_to() |
| 31 | + .clone() |
| 32 | + .into_text() |
| 33 | + .map(|text| text.to_string()), |
| 34 | + references, |
| 35 | + ) |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + impl Display for References { |
| 40 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 41 | + write!(f, "In Reply To: {:?}", self.0)?; |
| 42 | + for (i, reference) in self.1.iter().enumerate() { |
| 43 | + if i == 0 { |
| 44 | + writeln!(f, "\nReferences:")?; |
| 45 | + } |
| 46 | + writeln!(f, "\t{}", reference)?; |
| 47 | + } |
| 48 | + Ok(()) |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | #[derive(Clone, Default)] |
| 53 | pub struct Summary { |
| 54 | pub successful: Vec<String>, |
| 55 | @@ -57,7 +91,14 @@ pub async fn deliver_all(config: &Config) -> Result<Summary, Error> { |
| 56 | .parse(&message_bytes) |
| 57 | .expect("Cannot parse message"); |
| 58 | |
| 59 | - let message_id = entry.id(); |
| 60 | + let message_id = message.message_id(); |
| 61 | + |
| 62 | + if message_id.is_none() { |
| 63 | + return Err(Error::NoMessageId(entry.id().to_string())); |
| 64 | + } |
| 65 | + |
| 66 | + let message_id = message_id.unwrap(); |
| 67 | + |
| 68 | let mail_to = [ |
| 69 | addresses(message.to()), |
| 70 | // addresses(message.cc()), |
| 71 | @@ -75,10 +116,9 @@ pub async fn deliver_all(config: &Config) -> Result<Summary, Error> { |
| 72 | .address() |
| 73 | .unwrap(); |
| 74 | |
| 75 | - let reply_to = message |
| 76 | - .reply_to() |
| 77 | - .and_then(|address| address.first()) |
| 78 | - .and_then(|address| address.address()); |
| 79 | + let in_reply_to = message.in_reply_to().as_text(); |
| 80 | + |
| 81 | + println!("IN REPLY TO: {:?}", in_reply_to); |
| 82 | |
| 83 | tracing::info!( |
| 84 | "Attempting to deliver message from {} to {:?}", |
| 85 | @@ -92,7 +132,7 @@ pub async fn deliver_all(config: &Config) -> Result<Summary, Error> { |
| 86 | mail_to.as_slice(), |
| 87 | mail_from, |
| 88 | message_bytes.as_slice(), |
| 89 | - reply_to, |
| 90 | + in_reply_to, |
| 91 | ) |
| 92 | .await |
| 93 | { |
| 94 | @@ -107,9 +147,10 @@ pub async fn deliver_all(config: &Config) -> Result<Summary, Error> { |
| 95 | } else { |
| 96 | summary.successful.push(message_id.to_string()); |
| 97 | tracing::info!("Delivered message {}", message_id); |
| 98 | - if entry.is_seen() { |
| 99 | - maildir.move_new_to_cur(message_id)?; |
| 100 | - } |
| 101 | + // FIXME |
| 102 | + // if entry.is_seen() { |
| 103 | + // maildir.move_new_to_cur(message_id)?; |
| 104 | + // } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | diff --git a/ayllu-mail/src/error.rs b/ayllu-mail/src/error.rs |
| 109 | index f968236..9efdb2b 100644 |
| 110 | --- a/ayllu-mail/src/error.rs |
| 111 | +++ b/ayllu-mail/src/error.rs |
| 112 | @@ -12,5 +12,7 @@ pub enum Error { |
| 113 | #[error("Io: {0}")] |
| 114 | Io(#[from] IoError), |
| 115 | #[error("Maildir: {0}")] |
| 116 | - Maildir(#[from] MaildirError) |
| 117 | + Maildir(#[from] MaildirError), |
| 118 | + #[error("Message has no message id: {0}")] |
| 119 | + NoMessageId(String) |
| 120 | } |
| 121 | diff --git a/ayllu/src/web2/routes/mail.rs b/ayllu/src/web2/routes/mail.rs |
| 122 | index 0bd0827..5c4e35e 100644 |
| 123 | --- a/ayllu/src/web2/routes/mail.rs |
| 124 | +++ b/ayllu/src/web2/routes/mail.rs |
| 125 | @@ -7,7 +7,7 @@ use axum::{ |
| 126 | http::header::CONTENT_TYPE, |
| 127 | response::{Html, IntoResponse, Response}, |
| 128 | }; |
| 129 | - use ayllu_database::{mail::MailExt, Wrapper as Database}; |
| 130 | + use ayllu_database::{mail::{ListId, MailExt}, Wrapper as Database}; |
| 131 | use serde::{Deserialize, Serialize}; |
| 132 | |
| 133 | use crate::web2::middleware::template::Template; |
| 134 | @@ -79,12 +79,16 @@ pub async fn threads( |
| 135 | .lists |
| 136 | .iter() |
| 137 | .find(|list| list.name == params.list_name) |
| 138 | - .ok_or(Error::NotFound(params.list_name))?; |
| 139 | + .ok_or(Error::NotFound(params.list_name.clone()))?; |
| 140 | ctx.insert("list", &mailing_list); |
| 141 | |
| 142 | // FIXME |
| 143 | - let threads: Vec<Thread> = Vec::new(); |
| 144 | + // |
| 145 | + let list_id = ListId(params.list_name.clone(), String::default()); |
| 146 | + let threads = db.read_threads(&list_id).await?; |
| 147 | + let first_message = threads.first(); |
| 148 | ctx.insert("threads", &threads); |
| 149 | + ctx.insert("first_message", &first_message); |
| 150 | let body = templates.render("threads.html", &ctx)?; |
| 151 | Ok(Html(body)) |
| 152 | // let mailing_list = lists.iter().find(|list| list.id == params.list_id); |
| 153 | diff --git a/ayllu/themes/default/templates/threads.html b/ayllu/themes/default/templates/threads.html |
| 154 | index 57fd7a1..4beb63f 100644 |
| 155 | --- a/ayllu/themes/default/templates/threads.html |
| 156 | +++ b/ayllu/themes/default/templates/threads.html |
| 157 | @@ -28,15 +28,10 @@ |
| 158 | <h4>Threads</h4> |
| 159 | <div class="stretch"> |
| 160 | {% for thread in threads %} |
| 161 | - <section class="card"> |
| 162 | - {% set thread = thread.first %} |
| 163 | - <article class="segmented-3"> |
| 164 | - <div>{{ thread.from }}</div> |
| 165 | - <div>{{ thread.timestamp | format_epoch }}</div> |
| 166 | - <div>{{ thread.n_replies }}</div> |
| 167 | - </article> |
| 168 | + <div>{{ thread.from }}</div> |
| 169 | + <div>{{ thread.timestamp | format_epoch }}</div> |
| 170 | + <div>{{ thread.n_replies }}</div> |
| 171 | <a href="/mail/thread/{{ list.id }}/{{ thread.message_id }}">{{ thread.subject }}</a> |
| 172 | - </section> |
| 173 | {% endfor %} |
| 174 | </div> |
| 175 | </article> |
| 176 | diff --git a/crates/database/queries/mail_deliver_message.sql b/crates/database/queries/mail_deliver_message.sql |
| 177 | index 7eb5300..aa8339e 100644 |
| 178 | --- a/crates/database/queries/mail_deliver_message.sql |
| 179 | +++ b/crates/database/queries/mail_deliver_message.sql |
| 180 | @@ -1,7 +1,8 @@ |
| 181 | INSERT INTO messages |
| 182 | - (message_id, list_id, reply_to, mail_from, message_body) |
| 183 | + (message_id, list_id, in_reply_to, mail_from, message_body) |
| 184 | VALUES ( |
| 185 | ?, |
| 186 | ( SELECT id FROM lists WHERE address = ? LIMIT 1 ), |
| 187 | - ?, ?, ? |
| 188 | + ( SELECT id FROM messages WHERE message_id = ? LIMIT 1), |
| 189 | + ?, ? |
| 190 | ) RETURNING messages.id |
| 191 | diff --git a/crates/database/queries/mail_read_threads.sql b/crates/database/queries/mail_read_threads.sql |
| 192 | index c76aa55..6d85b19 100644 |
| 193 | --- a/crates/database/queries/mail_read_threads.sql |
| 194 | +++ b/crates/database/queries/mail_read_threads.sql |
| 195 | @@ -1,3 +1,3 @@ |
| 196 | SELECT messages.* FROM messages |
| 197 | LEFT JOIN lists ON lists.id = messages.id |
| 198 | - WHERE lists.name = ? AND messages.reply_to IS NULL |
| 199 | + WHERE lists.name = ? AND messages.in_reply_to IS NULL |
| 200 | diff --git a/crates/database/queries/mail_thread_count.sql b/crates/database/queries/mail_thread_count.sql |
| 201 | index a021ac4..5dc619a 100644 |
| 202 | --- a/crates/database/queries/mail_thread_count.sql |
| 203 | +++ b/crates/database/queries/mail_thread_count.sql |
| 204 | @@ -1,4 +1,4 @@ |
| 205 | SELECT COUNT(*) AS "count: i64" FROM messages |
| 206 | WHERE |
| 207 | list_id = (SELECT list_id FROM lists WHERE list_id = ?) AND |
| 208 | - reply_to IS NULL |
| 209 | + in_reply_to IS NULL |
| 210 | diff --git a/crates/database/src/mail.rs b/crates/database/src/mail.rs |
| 211 | index dfcc629..9077a2f 100644 |
| 212 | --- a/crates/database/src/mail.rs |
| 213 | +++ b/crates/database/src/mail.rs |
| 214 | @@ -49,7 +49,7 @@ pub struct Message { |
| 215 | pub id: i64, |
| 216 | pub message_id: String, |
| 217 | pub list_id: i64, |
| 218 | - pub reply_to: Option<i64>, |
| 219 | + pub in_reply_to: Option<i64>, |
| 220 | pub mail_from: i64, |
| 221 | pub message_body: Option<Vec<u8>>, |
| 222 | } |
| 223 | @@ -60,6 +60,12 @@ impl Message { |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | + #[derive(Clone, Default, Serialize, Deserialize)] |
| 228 | + pub struct ThreadSummary { |
| 229 | + pub first_message: Message, |
| 230 | + pub n_replies: i64 |
| 231 | + } |
| 232 | + |
| 233 | #[async_trait] |
| 234 | pub trait MailExt { |
| 235 | /// Ensure that the mailing list configuration reflects the state of the |
| 236 | @@ -76,7 +82,7 @@ pub trait MailExt { |
| 237 | mail_to: &[&str], |
| 238 | mail_from: &str, |
| 239 | message_body: &[u8], |
| 240 | - reply_to: Option<&str>, |
| 241 | + in_reply_to: Option<&str>, |
| 242 | ) -> Result<Vec<i64>, Error>; |
| 243 | |
| 244 | async fn has_message(&self, message_id: &str) -> Result<bool, Error>; |
| 245 | @@ -85,7 +91,7 @@ pub trait MailExt { |
| 246 | |
| 247 | async fn read_message(&self, list_id: &ListId, message_id: &str) -> Result<Message, Error>; |
| 248 | async fn read_thread(&self, list_id: &ListId, message_id: &str) -> Result<Thread, Error>; |
| 249 | - async fn read_threads(&self, list_id: &ListId) -> Result<Vec<Message>, Error>; |
| 250 | + async fn read_threads(&self, list_id: &ListId) -> Result<Vec<ThreadSummary>, Error>; |
| 251 | } |
| 252 | |
| 253 | #[async_trait] |
| 254 | @@ -157,7 +163,7 @@ impl MailExt for Database { |
| 255 | mail_to: &[&str], |
| 256 | mail_from: &str, |
| 257 | message_body: &[u8], |
| 258 | - reply_to: Option<&str>, |
| 259 | + in_reply_to: Option<&str>, |
| 260 | ) -> Result<Vec<i64>, Error> { |
| 261 | let mut tx = self.pool.begin().await?; |
| 262 | let participant_id = sqlx::query_file!( |
| 263 | @@ -175,7 +181,7 @@ impl MailExt for Database { |
| 264 | "queries/mail_deliver_message.sql", |
| 265 | message_id, |
| 266 | mail_to_addr, |
| 267 | - reply_to, |
| 268 | + in_reply_to, |
| 269 | participant_id, |
| 270 | message_body, |
| 271 | ) |
| 272 | @@ -216,11 +222,11 @@ impl MailExt for Database { |
| 273 | Ok(Thread(vec![message])) |
| 274 | } |
| 275 | |
| 276 | - async fn read_threads(&self, list_id: &ListId) -> Result<Vec<Message>, Error> { |
| 277 | + async fn read_threads(&self, list_id: &ListId) -> Result<Vec<ThreadSummary>, Error> { |
| 278 | let list_name = list_id.0.as_str(); |
| 279 | let messages = sqlx::query_file_as!(Message, "queries/mail_read_threads.sql", list_name) |
| 280 | .fetch_all(&self.pool) |
| 281 | .await?; |
| 282 | - Ok(messages) |
| 283 | + Ok(Vec::new()) |
| 284 | } |
| 285 | } |
| 286 | diff --git a/migrations/20241028092919_mail.sql b/migrations/20241028092919_mail.sql |
| 287 | index e5f36cb..a346854 100644 |
| 288 | --- a/migrations/20241028092919_mail.sql |
| 289 | +++ b/migrations/20241028092919_mail.sql |
| 290 | @@ -31,7 +31,7 @@ CREATE TABLE messages ( |
| 291 | id INTEGER PRIMARY KEY, |
| 292 | message_id TEXT NOT NULL UNIQUE, |
| 293 | list_id INTEGER NOT NULL REFERENCES lists(id), |
| 294 | - reply_to INTEGER REFERENCES messages(id), |
| 295 | + in_reply_to INTEGER REFERENCES messages(id), |
| 296 | mail_from INTEGER NOT NULL REFERENCES participants(id), |
| 297 | message_body BLOB |
| 298 | ) STRICT ; |