Commit
+279 -95 +/-12 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 68b08a3..4aeb8e0 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -138,9 +138,9 @@ dependencies = [ |
6 | |
7 | [[package]] |
8 | name = "anyhow" |
9 | - version = "1.0.75" |
10 | + version = "1.0.78" |
11 | source = "registry+https://github.com/rust-lang/crates.io-index" |
12 | - checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" |
13 | + checksum = "ca87830a3e3fb156dc96cfbd31cb620265dd053be734723f22b760d6cc3c3051" |
14 | |
15 | [[package]] |
16 | name = "arrayvec" |
17 | @@ -585,6 +585,7 @@ dependencies = [ |
18 | name = "ayllu-mail" |
19 | version = "0.2.1" |
20 | dependencies = [ |
21 | + "anyhow", |
22 | "ayllu_api", |
23 | "ayllu_config", |
24 | "ayllu_rpc", |
25 | @@ -2895,7 +2896,7 @@ dependencies = [ |
26 | [[package]] |
27 | name = "mailpot" |
28 | version = "0.1.1" |
29 | - source = "git+https://ayllu-forge.org/forks/mailpot?branch=ayllu-dev#27dd84e1ff491418832cc64b2758e36197a6c6ba" |
30 | + source = "git+https://ayllu-forge.org/forks/mailpot?branch=ayllu-dev#30a34815d96cc642963ea9aaa5b9c4f36db459d2" |
31 | dependencies = [ |
32 | "anyhow", |
33 | "chrono", |
34 | diff --git a/ayllu-mail/Cargo.toml b/ayllu-mail/Cargo.toml |
35 | index 5fb51f0..afb1c6b 100644 |
36 | --- a/ayllu-mail/Cargo.toml |
37 | +++ b/ayllu-mail/Cargo.toml |
38 | @@ -18,3 +18,4 @@ capnp = "0.18.1" |
39 | melib = "0.8.2" |
40 | mailpot = { git = "https://ayllu-forge.org/forks/mailpot", branch = "ayllu-dev"} |
41 | clap_complete = "4.4.5" |
42 | + anyhow = "1.0.78" |
43 | diff --git a/ayllu-mail/src/declarative.rs b/ayllu-mail/src/declarative.rs |
44 | index 0708e12..ad1dfd7 100644 |
45 | --- a/ayllu-mail/src/declarative.rs |
46 | +++ b/ayllu-mail/src/declarative.rs |
47 | @@ -1,7 +1,7 @@ |
48 | use std::collections::HashMap; |
49 | |
50 | use mailpot::{ |
51 | - models::{MailingList, PostPolicy, SubscriptionPolicy}, |
52 | + models::{changesets::MailingListChangeset, MailingList, PostPolicy, SubscriptionPolicy}, |
53 | Connection, Result as MpResult, |
54 | }; |
55 | use tracing::log; |
56 | @@ -23,7 +23,6 @@ pub fn initialize(cfg: &Config) -> MpResult<()> { |
57 | |
58 | for list in cfg.mail.lists.iter() { |
59 | log::info!("configuring mailing list: {} [{}]", list.id, list.address); |
60 | - // TODO: confused on distinciton between id and name |
61 | if !lists_by_name.contains_key(&list.id) { |
62 | log::info!("creating mailing list: {}", list.address); |
63 | let db_list = db.create_list(MailingList { |
64 | @@ -36,6 +35,18 @@ pub fn initialize(cfg: &Config) -> MpResult<()> { |
65 | archive_url: None, |
66 | })?; |
67 | lists_by_name.insert(db_list.id.clone(), db_list.pk); |
68 | + } else { |
69 | + let pk = *lists_by_name.get(&list.id).unwrap(); |
70 | + // ensure the configuration is consistent |
71 | + db.update_list(MailingListChangeset { |
72 | + pk, |
73 | + // TODO: topics cannot be updated? |
74 | + description: list |
75 | + .description |
76 | + .as_ref() |
77 | + .map(|description| Some(description.clone())), |
78 | + ..Default::default() |
79 | + })?; |
80 | } |
81 | managed_lists.push(list.id.clone()); |
82 | } |
83 | @@ -45,7 +56,7 @@ pub fn initialize(cfg: &Config) -> MpResult<()> { |
84 | for list in cfg.mail.lists.iter() { |
85 | let list_pk = *lists_by_name.get(&list.id).unwrap(); |
86 | let post_policy = PostPolicy { |
87 | - pk: 0, // TODO ? |
88 | + pk: 0, |
89 | list: list_pk, |
90 | announce_only: matches!(list.post_policy, config::PostPolicy::AnnounceOnly), |
91 | subscription_only: matches!(list.post_policy, config::PostPolicy::SubscriptionOnly), |
92 | @@ -93,11 +104,20 @@ pub fn initialize(cfg: &Config) -> MpResult<()> { |
93 | .collect(); |
94 | |
95 | if extras.len() > 0 { |
96 | - // TODO: there is no way to delete lists currently |
97 | log::info!("database contains the following superfluous lists:"); |
98 | extras.iter().for_each(|extra| { |
99 | log::info!("List: {}", extra); |
100 | }); |
101 | + for extra in extras.iter() { |
102 | + log::info!("disabling mailing list: {}", extra); |
103 | + let pk = *lists_by_name.get(&extra.to_string()).unwrap(); |
104 | + db.update_list(MailingListChangeset { |
105 | + pk, |
106 | + enabled: Some(false), |
107 | + hidden: Some(true), |
108 | + ..Default::default() |
109 | + })?; |
110 | + } |
111 | } |
112 | |
113 | Ok(()) |
114 | diff --git a/ayllu-mail/src/server.rs b/ayllu-mail/src/server.rs |
115 | index f21b111..9822a1c 100644 |
116 | --- a/ayllu-mail/src/server.rs |
117 | +++ b/ayllu-mail/src/server.rs |
118 | @@ -2,15 +2,20 @@ use std::collections::HashMap; |
119 | use std::error::Error as StdError; |
120 | use std::sync::{Arc, RwLock}; |
121 | |
122 | + use anyhow::{format_err, Result}; |
123 | use capnp::{capability::Promise, Error as CapnpError}; |
124 | use capnp_rpc::pry; |
125 | - use mailpot::{models::Post, Connection}; |
126 | + use mailpot::{ |
127 | + models::{DbVal, Post}, |
128 | + Connection, Error as MailpotError, |
129 | + }; |
130 | use melib::{thread::Threads, Envelope, EnvelopeHash}; |
131 | use tracing::log; |
132 | |
133 | use crate::config::Config; |
134 | use ayllu_api::mail_capnp::server::{ |
135 | - Client, ListThreadsParams, ListThreadsResults, ReadThreadParams, ReadThreadResults, Server, |
136 | + Client, ListThreadsParams, ListThreadsResults, ReadPostParams, ReadPostResults, |
137 | + ReadThreadParams, ReadThreadResults, Server, |
138 | }; |
139 | use ayllu_rpc::Server as RpcHelper; |
140 | |
141 | @@ -22,67 +27,139 @@ impl Server for ServerImpl { |
142 | fn list_threads( |
143 | &mut self, |
144 | params: ListThreadsParams, |
145 | - mut results: ListThreadsResults, |
146 | + results: ListThreadsResults, |
147 | ) -> Promise<(), CapnpError> { |
148 | - let mailing_list_name = pry!(pry!(pry!(params.get()).get_name()).to_string()); |
149 | - log::info!("looking up threads: {}", mailing_list_name); |
150 | - for list in self.db.lists().unwrap() { |
151 | - if list.address == mailing_list_name { |
152 | - let posts = self.db.list_posts(list.pk, None).unwrap(); |
153 | - log::info!("processing {} posts", posts.len()); |
154 | - let mut posts_by_hash: HashMap<EnvelopeHash, Post> = HashMap::new(); |
155 | - let mut envelopes: HashMap<EnvelopeHash, Envelope> = HashMap::new(); |
156 | - for post in &posts { |
157 | - let envelope = Envelope::from_bytes(&post.message, None).unwrap(); |
158 | - let envelope_hash = envelope.hash; |
159 | - posts_by_hash.insert(envelope_hash, post.0.clone()); |
160 | - envelopes.insert(envelope_hash, envelope); |
161 | - } |
162 | - let mut threads = Threads::new(posts.len()); |
163 | - let envelopes = Arc::new(RwLock::new(envelopes)); |
164 | - threads.amend(&envelopes); |
165 | - let envelopes = envelopes.read().unwrap(); |
166 | - let mut root_threads: Vec<(Post, Envelope, usize)> = Vec::new(); |
167 | - for node in threads.thread_nodes.into_iter() { |
168 | - if !node.1.has_parent() { |
169 | - let n_replies = node.1.children.len(); |
170 | - let envelope_hash = node.1.message().unwrap(); |
171 | - let envelope = envelopes.get(&envelope_hash).unwrap(); |
172 | - let post = posts_by_hash.get(&envelope_hash).unwrap(); |
173 | - root_threads.push((post.clone(), envelope.clone(), n_replies)); |
174 | - } |
175 | - } |
176 | - let mut threads = results.get().init_threads(root_threads.len() as u32); |
177 | - for (i, thread) in root_threads.into_iter().enumerate() { |
178 | - let mut result_thread = threads.reborrow().get(i as u32); |
179 | - result_thread.set_n_replies(thread.2 as i64); |
180 | - let mut message = result_thread.init_first(); |
181 | - message.set_id(thread.0.pk); |
182 | - message.set_address(thread.0.address.as_str().into()); |
183 | - message.set_from(thread.1.from()[0].get_email().as_str().into()); |
184 | - message.set_subject(thread.1.subject.unwrap_or(String::new()).as_str().into()); |
185 | - message.set_timestamp(thread.1.timestamp as i64); |
186 | + let list_id = pry!(pry!(params.get()).get_id()).to_string().unwrap(); |
187 | + let list_threads = |mut results: ListThreadsResults| { |
188 | + let list = self |
189 | + .db |
190 | + .list_by_id(&list_id)? |
191 | + .ok_or(format_err!("no mailing list with id: {}", &list_id))?; |
192 | + let posts = self.db.list_posts(list.pk, None)?; |
193 | + // TODO: add a new database method to only return posts without |
194 | + // reply headers to save cpu cycles |
195 | + let posts: Vec<&DbVal<Post>> = posts |
196 | + .iter() |
197 | + .filter(|post| { |
198 | + let envelope = Envelope::from_bytes(&post.0.message, None).unwrap(); |
199 | + envelope.in_reply_to.is_none() |
200 | + }) |
201 | + .collect(); |
202 | + let mut threads = results.get().init_threads(posts.len() as u32); |
203 | + for (i, post) in posts.iter().enumerate() { |
204 | + let envelope = Envelope::from_bytes(&post.0.message, None).unwrap(); |
205 | + let message_text = envelope.body_bytes(&post.0.message).text(); |
206 | + let replies = self.db.list_thread(list.pk, &post.message_id)?; |
207 | + let mut thread = threads.reborrow().get(i as u32); |
208 | + thread.reborrow().set_n_replies(replies.len() as i64); |
209 | + let mut first = thread.get_first().unwrap(); |
210 | + first.set_from(envelope.from[0].get_email().as_str().into()); |
211 | + first.set_id(post.pk); |
212 | + first.set_address(post.0.address.as_str().into()); |
213 | + first.set_message_id(post.0.message_id.as_str().into()); |
214 | + first.set_timestamp(post.0.timestamp as i64); |
215 | + if let Some(subject) = envelope.subject { |
216 | + first.set_subject(subject.as_str().into()); |
217 | } |
218 | + let message = String::from_utf8(post.0.message.clone()).unwrap(); |
219 | + first.set_body(message.as_str().into()); |
220 | + first.set_text(message_text.as_str().into()); |
221 | } |
222 | + Ok::<(), MailpotError>(()) |
223 | + }; |
224 | + match list_threads(results) { |
225 | + Ok(_) => Promise::ok(()), |
226 | + Err(e) => Promise::err(CapnpError::failed(e.to_string())), |
227 | } |
228 | - Promise::ok(()) |
229 | } |
230 | |
231 | fn read_thread( |
232 | &mut self, |
233 | params: ReadThreadParams, |
234 | - mut results: ReadThreadResults, |
235 | + results: ReadThreadResults, |
236 | + ) -> Promise<(), CapnpError> { |
237 | + let params = pry!(params.get()); |
238 | + let list_id = pry!(pry!(params.get_id()).to_string()); |
239 | + let message_id = pry!(pry!(params.get_message_id()).to_string()); |
240 | + let read_thread = |mut results: ReadThreadResults| { |
241 | + let list = self |
242 | + .db |
243 | + .list_by_id(&list_id)? |
244 | + .ok_or(format_err!("no mailing list with id: {}", list_id))?; |
245 | + let first_post = self |
246 | + .db |
247 | + .list_post_by_message_id(list.pk, &message_id)? |
248 | + .ok_or(format_err!("cannot find post: {}", message_id))?; |
249 | + let replies = self.db.list_thread(list.pk, &message_id)?; |
250 | + let mut threads = results.get().init_thread(replies.len() as u32 + 1); |
251 | + let mut first = threads.reborrow().get(0); |
252 | + first.set_id(first_post.0.pk); |
253 | + first.set_address(first_post.0.address.as_str().into()); |
254 | + first.set_message_id(first_post.0.message_id.as_str().into()); |
255 | + let envelope = Envelope::from_bytes(&first_post.0.message, None).unwrap(); |
256 | + let message_text = envelope.body_bytes(&first_post.0.message).text(); |
257 | + first.set_text(message_text.as_str().into()); |
258 | + let message_body = String::from_utf8(first_post.0.message).unwrap(); |
259 | + first.set_body(message_body.as_str().into()); |
260 | + first.set_timestamp(first_post.0.timestamp as i64); |
261 | + for (i, reply) in replies.iter().enumerate() { |
262 | + let mut next = threads.reborrow().get(i as u32 + 1); |
263 | + let envelope = Envelope::from_bytes(&reply.1.message, None).unwrap(); |
264 | + next.set_id(reply.1.pk); |
265 | + next.set_address(reply.1.address.as_str().into()); |
266 | + next.set_message_id(reply.1 .0.message_id.as_str().into()); |
267 | + let message_body = String::from_utf8(reply.1.message.to_vec()).unwrap(); |
268 | + next.set_body(message_body.as_str().into()); |
269 | + let message_text = envelope.body_bytes(&reply.1.message).text(); |
270 | + next.set_text(message_text.as_str().into()); |
271 | + } |
272 | + Ok::<(), MailpotError>(()) |
273 | + }; |
274 | + match read_thread(results) { |
275 | + Ok(_) => Promise::ok(()), |
276 | + Err(e) => Promise::err(CapnpError::failed(e.to_string())), |
277 | + } |
278 | + } |
279 | + |
280 | + fn read_post( |
281 | + &mut self, |
282 | + params: ReadPostParams, |
283 | + results: ReadPostResults, |
284 | ) -> Promise<(), CapnpError> { |
285 | let params = pry!(params.get()); |
286 | - let (thread_id, offset, limit) = (params.get_id(), params.get_offset(), params.get_limit()); |
287 | - ::capnp::capability::Promise::err(::capnp::Error::unimplemented( |
288 | - "method server::Server::read_thread not implemented".to_string(), |
289 | - )) |
290 | + let list_id = pry!(pry!(params.get_id()).to_string()); |
291 | + let message_id = pry!(pry!(params.get_message_id()).to_string()); |
292 | + let read_post = |mut results: ReadPostResults| { |
293 | + let list = self |
294 | + .db |
295 | + .list_by_id(&list_id)? |
296 | + .ok_or(format_err!("no mailing list with id: {}", list_id))?; |
297 | + let post = self |
298 | + .db |
299 | + .list_post_by_message_id(list.pk, &message_id)? |
300 | + .ok_or(format_err!("failed to find post with id: {}", message_id))?; |
301 | + let mut message = results.get(); |
302 | + let envelope = Envelope::from_bytes(&post.message, None).unwrap(); |
303 | + message.set_id(post.pk); |
304 | + message.set_address(post.address.as_str().into()); |
305 | + if let Some(from) = post.envelope_from.as_ref() { |
306 | + message.set_from(from.as_str().into()); |
307 | + } |
308 | + let message_body = String::from_utf8(post.message.to_vec()).unwrap(); |
309 | + message.set_body(message_body.as_str().into()); |
310 | + let message_text = envelope.body_bytes(&post.message).text(); |
311 | + message.set_text(message_text.as_str().into()); |
312 | + Ok::<(), MailpotError>(()) |
313 | + }; |
314 | + match read_post(results) { |
315 | + Ok(_) => Promise::ok(()), |
316 | + Err(e) => Promise::err(CapnpError::failed(e.to_string())), |
317 | + } |
318 | } |
319 | } |
320 | |
321 | pub async fn serve(cfg: &Config) -> Result<(), Box<dyn StdError>> { |
322 | - let db = Connection::open_or_create_db(cfg.mailpot_config())?; |
323 | + let db = Connection::open_or_create_db(cfg.mailpot_config())?.trusted(); |
324 | let server = ServerImpl { db }; |
325 | let runtime = RpcHelper::<Client, ServerImpl>::new(&cfg.mail.socket_path, server); |
326 | runtime.serve().await?; |
327 | diff --git a/crates/api/v1/mail.capnp b/crates/api/v1/mail.capnp |
328 | index 797f45d..eb951e7 100644 |
329 | --- a/crates/api/v1/mail.capnp |
330 | +++ b/crates/api/v1/mail.capnp |
331 | @@ -1,14 +1,23 @@ |
332 | @0xe282ac1f72de3195; |
333 | |
334 | + struct ThreadedMessage { |
335 | + depth @0 :Int64; |
336 | + message @1 :Message; |
337 | + } |
338 | + |
339 | # a single email |
340 | struct Message { |
341 | # primary key of the message in the mailpot db |
342 | id @0 :Int64; |
343 | - timestamp @1 :Int64; |
344 | - from @2 :Text; |
345 | - address @3 :Text; |
346 | - subject @4 :Text; |
347 | - body @5 :Text; |
348 | + messageId @1 :Text; |
349 | + timestamp @2 :Int64; |
350 | + from @3 :Text; |
351 | + address @4 :Text; |
352 | + subject @5 :Text; |
353 | + # entire email including headers |
354 | + body @6 :Text; |
355 | + # text of just the email message |
356 | + text @7 :Text; |
357 | } |
358 | |
359 | struct Thread { |
360 | @@ -20,7 +29,9 @@ struct Thread { |
361 | |
362 | interface Server { |
363 | # list all of the threads associated with a mailing list |
364 | - listThreads @0 (name: Text, offset: Int64, limit: Int64) -> (threads: List(Thread)); |
365 | - # list all of the responses associated with a post |
366 | - readThread @1 (id: Int64, offset: Int64, limit: Int64) -> (thread: List(Message)); |
367 | + listThreads @0 (id: Text, offset: Int64, limit: Int64) -> (threads: List(Thread)); |
368 | + # list all of the responses associated with a post starting from messageId |
369 | + readThread @1 (id: Text, messageId: Text, offset: Int64, limit: Int64) -> (thread: List(Message)); |
370 | + # read a single post |
371 | + readPost @2 (id: Text, messageId: Text) -> Message; |
372 | } |
373 | diff --git a/scripts/watch.sh b/scripts/watch.sh |
374 | index 6f3cf4b..24d9916 100755 |
375 | --- a/scripts/watch.sh |
376 | +++ b/scripts/watch.sh |
377 | @@ -19,6 +19,8 @@ if [ "${COMPONENT}" = "." ] ; then |
378 | -s 'scripts/compile_stylesheets.sh && cargo run -- serve' |
379 | elif [ "${COMPONENT}" = "ayllu-build" ] ; then |
380 | cargo watch -s 'cargo run -- serve' |
381 | + elif [ "${COMPONENT}" = "ayllu-mail" ] ; then |
382 | + cargo watch -s 'cargo run -- serve' |
383 | else |
384 | cargo watch -x run |
385 | fi |
386 | diff --git a/src/web2/routes/mail.rs b/src/web2/routes/mail.rs |
387 | index e795a8f..d835715 100644 |
388 | --- a/src/web2/routes/mail.rs |
389 | +++ b/src/web2/routes/mail.rs |
390 | @@ -4,7 +4,6 @@ use axum::{ |
391 | response::Html, |
392 | }; |
393 | use serde::{Deserialize, Serialize}; |
394 | - use time::OffsetDateTime; |
395 | |
396 | use crate::config::Config; |
397 | use crate::web2::error::Error; |
398 | @@ -16,25 +15,28 @@ use ayllu_api::mail_capnp::server::Client as MailClient; |
399 | |
400 | #[derive(Deserialize)] |
401 | pub struct Params { |
402 | - pub address: String, |
403 | - pub thread_id: Option<i64>, |
404 | + pub list_id: String, |
405 | + pub message_id: Option<String>, |
406 | } |
407 | |
408 | #[derive(Debug, Serialize, Default)] |
409 | struct Thread { |
410 | pub id: i64, |
411 | + pub message_id: String, |
412 | pub from: String, |
413 | pub subject: String, |
414 | pub n_replies: i64, |
415 | - pub timestamp: String, |
416 | + pub timestamp: i64, |
417 | } |
418 | |
419 | #[derive(Debug, Serialize, Default)] |
420 | struct Message { |
421 | pub id: String, |
422 | - pub created_at: String, |
423 | + pub message_id: String, |
424 | + pub created_at: i64, |
425 | pub from_address: String, |
426 | pub body: String, |
427 | + pub text: String, |
428 | } |
429 | |
430 | pub async fn lists( |
431 | @@ -50,6 +52,7 @@ pub async fn lists( |
432 | let body = templates.render("lists.html", &ctx)?; |
433 | Ok(Html(body)) |
434 | } |
435 | + |
436 | #[debug_handler] |
437 | pub async fn threads( |
438 | Path(params): Path<Params>, |
439 | @@ -59,42 +62,42 @@ pub async fn threads( |
440 | Extension((templates, mut ctx)): Extension<Template>, |
441 | ) -> Result<Html<String>, Error> { |
442 | let lists = cfg.mail.unwrap().lists; |
443 | - let list = match lists.iter().find(|list| list.address == params.address) { |
444 | + let list = match lists.iter().find(|list| list.id == params.list_id) { |
445 | Some(list) => Ok(list), |
446 | None => Err(Error::Message(format!( |
447 | "no list associated with: {}", |
448 | - params.address |
449 | + params.list_id |
450 | ))), |
451 | }?; |
452 | ctx.insert("title", &format!("list {}", list.address)); |
453 | ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
454 | ctx.insert("list", list); |
455 | - ctx.insert("address", ¶ms.address); |
456 | ctx.insert("discnav", &util::navigation::discnav("mail")); |
457 | |
458 | let mail_client = initiator.client(InitiatorKind::Mail).unwrap(); |
459 | - let threads = mail_client |
460 | + let mut threads = mail_client |
461 | .invoke(move |c: MailClient| async move { |
462 | let mut threads: Vec<Thread> = Vec::new(); |
463 | let mut req = c.list_threads_request(); |
464 | - req.get().set_name(params.address.as_str().into()); |
465 | + req.get().set_id(params.list_id.as_str().into()); |
466 | let result = req.send().promise.await?; |
467 | let rpc_threads = result.get()?.get_threads()?; |
468 | for thread in rpc_threads.iter() { |
469 | let message = thread.get_first()?; |
470 | - let timestamp = |
471 | - OffsetDateTime::from_unix_timestamp(message.get_timestamp()).unwrap(); |
472 | + let message_id = message.get_message_id()?.to_string().unwrap(); |
473 | threads.push(Thread { |
474 | id: message.get_id(), |
475 | + message_id, |
476 | from: message.get_from()?.to_string().unwrap(), |
477 | subject: message.get_subject()?.to_string().unwrap(), |
478 | n_replies: thread.get_n_replies(), |
479 | - timestamp: timestamp.to_string(), |
480 | + timestamp: thread.get_first().unwrap().get_timestamp(), |
481 | }) |
482 | } |
483 | Ok(threads) |
484 | }) |
485 | .await?; |
486 | + threads.sort_by(|first, second| second.timestamp.cmp(&first.timestamp)); |
487 | ctx.insert("threads", &threads); |
488 | let body = templates.render("threads.html", &ctx)?; |
489 | Ok(Html(body)) |
490 | @@ -107,11 +110,11 @@ pub async fn thread( |
491 | Extension((templates, mut ctx)): Extension<Template>, |
492 | ) -> Result<Html<String>, Error> { |
493 | let lists = cfg.mail.unwrap().lists; |
494 | - let list = match lists.iter().find(|list| list.address == params.address) { |
495 | + let list = match lists.iter().find(|list| list.id == params.list_id) { |
496 | Some(list) => Ok(list), |
497 | None => Err(Error::Message(format!( |
498 | "no list associated with: {}", |
499 | - params.address |
500 | + params.list_id |
501 | ))), |
502 | }?; |
503 | let mail_client = initiator.client(InitiatorKind::Mail).unwrap(); |
504 | @@ -119,14 +122,18 @@ pub async fn thread( |
505 | .invoke(move |c: MailClient| async move { |
506 | let mut messages: Vec<Message> = Vec::new(); |
507 | let mut req = c.read_thread_request(); |
508 | - req.get().set_id(params.thread_id.unwrap()); |
509 | + req.get().set_id(params.list_id.as_str().into()); |
510 | + req.get() |
511 | + .set_message_id(params.message_id.unwrap().as_str().into()); |
512 | let result = req.send().promise.await?; |
513 | for message in result.get()?.get_thread()? { |
514 | messages.push(Message { |
515 | id: message.get_id().to_string(), |
516 | - created_at: message.get_timestamp().to_string(), // TODO no epoch! |
517 | + message_id: message.get_message_id()?.to_string().unwrap(), |
518 | + created_at: message.get_timestamp(), |
519 | from_address: message.get_address()?.to_string().unwrap(), |
520 | body: message.get_body()?.to_string().unwrap(), |
521 | + text: message.get_text()?.to_string().unwrap(), |
522 | }) |
523 | } |
524 | Ok(messages) |
525 | @@ -136,7 +143,51 @@ pub async fn thread( |
526 | ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
527 | ctx.insert("discnav", &util::navigation::discnav("mail")); |
528 | ctx.insert("list", list); |
529 | - ctx.insert("message", &messages); |
530 | + ctx.insert("list_id", &list.id); |
531 | + ctx.insert("messages", &messages); |
532 | let body = templates.render("thread.html", &ctx)?; |
533 | Ok(Html(body)) |
534 | } |
535 | + |
536 | + pub async fn post( |
537 | + Path(params): Path<Params>, |
538 | + Extension(initiator): Extension<Initiator>, |
539 | + Extension(cfg): Extension<Config>, |
540 | + Extension((templates, mut ctx)): Extension<Template>, |
541 | + ) -> Result<Html<String>, Error> { |
542 | + let lists = cfg.mail.unwrap().lists; |
543 | + let list = match lists.iter().find(|list| list.id == params.list_id) { |
544 | + Some(list) => Ok(list), |
545 | + None => Err(Error::Message(format!( |
546 | + "no list associated with: {}", |
547 | + params.list_id |
548 | + ))), |
549 | + }?; |
550 | + let mail_client = initiator.client(InitiatorKind::Mail).unwrap(); |
551 | + let message = mail_client |
552 | + .invoke(move |c: MailClient| async move { |
553 | + let mut req = c.read_post_request(); |
554 | + req.get().set_id(params.list_id.as_str().into()); |
555 | + req.get() |
556 | + .set_message_id(params.message_id.unwrap().as_str().into()); |
557 | + let result = req.send().promise.await?; |
558 | + let message = result.get()?; |
559 | + Ok(Message { |
560 | + id: message.get_id().to_string(), |
561 | + message_id: message.get_message_id()?.to_string().unwrap(), |
562 | + created_at: message.get_timestamp(), |
563 | + body: message.get_body()?.to_string().unwrap(), |
564 | + text: message.get_text()?.to_string().unwrap(), |
565 | + from_address: message.get_address()?.to_string().unwrap(), |
566 | + }) |
567 | + }) |
568 | + .await?; |
569 | + ctx.insert("title", &format!("list {}", list.address)); |
570 | + ctx.insert("nav_elements", &navigation::global("dicsuss", true)); |
571 | + ctx.insert("discnav", &util::navigation::discnav("mail")); |
572 | + ctx.insert("list", list); |
573 | + ctx.insert("list_id", &list.id); |
574 | + ctx.insert("message", &message); |
575 | + let body = templates.render("post.html", &ctx)?; |
576 | + Ok(Html(body)) |
577 | + } |
578 | diff --git a/src/web2/server.rs b/src/web2/server.rs |
579 | index bb597ad..653173b 100644 |
580 | --- a/src/web2/server.rs |
581 | +++ b/src/web2/server.rs |
582 | @@ -186,8 +186,9 @@ pub async fn serve(cfg: &Config) -> Result<(), Box<dyn Error>> { |
583 | "/mail", |
584 | Router::new() |
585 | .route("/", routing::get(mail::lists)) |
586 | - .route("/:address", routing::get(mail::threads)) |
587 | - .route("/:address/:thread_id", routing::get(mail::thread)) |
588 | + .route("/:list_id", routing::get(mail::threads)) |
589 | + .route("/:list_id/:message_id", routing::get(mail::thread)) |
590 | + .route("/post/:list_id/:message_id", routing::get(mail::post)) |
591 | .layer(from_fn_with_state( |
592 | Arc::new((cfg.clone(), templates.clone(), mail_required_plugins)), |
593 | rpc_initiator::required, |
594 | diff --git a/themes/default/templates/lists.html b/themes/default/templates/lists.html |
595 | index 13631e6..bdb32ba 100644 |
596 | --- a/themes/default/templates/lists.html |
597 | +++ b/themes/default/templates/lists.html |
598 | @@ -8,14 +8,18 @@ |
599 | </header> |
600 | <table> |
601 | <thead> |
602 | + <th> id </th> |
603 | <th> name </th> |
604 | <th> description </th> |
605 | + <th> address </th> |
606 | </thead> |
607 | <tbody> |
608 | {% for list in lists %} |
609 | <tr> |
610 | - <td><a href="/discuss/mail/{{list.address}}">{{ list.address }}</a></td> |
611 | + <td><a href="/discuss/mail/{{list.id}}">{{ list.id }}</a></td> |
612 | + <td>{{ list.name }}</td> |
613 | <td>{{ list.description }}</td> |
614 | + <td>{{ list.address }}</td> |
615 | </tr> |
616 | {% endfor %} |
617 | </tbody> |
618 | diff --git a/themes/default/templates/post.html b/themes/default/templates/post.html |
619 | new file mode 100644 |
620 | index 0000000..8826000 |
621 | --- /dev/null |
622 | +++ b/themes/default/templates/post.html |
623 | @@ -0,0 +1,14 @@ |
624 | + {% extends "base.html" %} |
625 | + {% block content %} |
626 | + <section class="thread-view"> |
627 | + <article> |
628 | + <header> |
629 | + <b>From: {{ message.from_address }}</b></br> |
630 | + <b>To: ???</b></br> |
631 | + <b><a href="/discuss/mail/post/{{list_id}}/{{message.message_id}}">{{ message.message_id }}</a></b> |
632 | + <span class="right">{{ message.created_at | format_epoch }}</span> |
633 | + </header> |
634 | + <pre>{{ message.text }}</pre> |
635 | + </article> |
636 | + </section> |
637 | + {% endblock %} |
638 | diff --git a/themes/default/templates/thread.html b/themes/default/templates/thread.html |
639 | index f6997b9..764f9b7 100644 |
640 | --- a/themes/default/templates/thread.html |
641 | +++ b/themes/default/templates/thread.html |
642 | @@ -1,14 +1,16 @@ |
643 | {% extends "base.html" %} |
644 | {% block content %} |
645 | <section class="thread-view"> |
646 | + |
647 | + {% for reply in messages %} |
648 | <article> |
649 | - <header><b>{{ message.subject }}</b><span class="right">{{ message.created_at }}</span></header> |
650 | - <pre class="email-thread">{{ message.body }}</pre> |
651 | - </article> |
652 | - {% for reply in replies %} |
653 | - <article> |
654 | - <header><b>{{ reply.subject }}</b><span class="right">{{ reply.created_at }}</span></header> |
655 | - <pre>{{ reply.body }}</pre> |
656 | + <header> |
657 | + <b>From: {{ reply.from_address }}</b></br> |
658 | + <b>To: ???</b></br> |
659 | + <b><a href="/discuss/mail/post/{{list_id}}/{{reply.message_id}}">{{ reply.message_id }}</a></b> |
660 | + <span class="right">{{ reply.created_at | format_epoch }}</span> |
661 | + </header> |
662 | + <pre>{{ reply.text }}</pre> |
663 | </article> |
664 | {% endfor %} |
665 | </section> |
666 | diff --git a/themes/default/templates/threads.html b/themes/default/templates/threads.html |
667 | index 8e0cb4b..d7c7431 100644 |
668 | --- a/themes/default/templates/threads.html |
669 | +++ b/themes/default/templates/threads.html |
670 | @@ -4,7 +4,7 @@ |
671 | <section> |
672 | <article> |
673 | <header> |
674 | - {{ macros::navigation(items=discnav, title=address) }} |
675 | + {{ macros::navigation(items=discnav, title=list.id) }} |
676 | </header> |
677 | <table> |
678 | <thead> |
679 | @@ -16,9 +16,9 @@ |
680 | <tbody> |
681 | {% for thread in threads %} |
682 | <tr> |
683 | - <td><a href="/discuss/mail/{{address}}/{{thread.id}}">{{ thread.from }}</a></td> |
684 | - <td>{{thread.timestamp}}</td> |
685 | - <td>{{thread.subject}}</td> |
686 | + <td>{{ thread.from }}</a></td> |
687 | + <td>{{thread.timestamp | format_epoch }}</td> |
688 | + <td><a href="/discuss/mail/{{list.id}}/{{thread.message_id}}">{{thread.subject}}</a></td> |
689 | <td>{{thread.n_replies}}</td> |
690 | </tr> |
691 | {% endfor %} |