1use crate::state::{ContainerLease, ContainerStateLock};
4use crate::suite::TestContainerSuite;
5use crate::wait::wait_until;
6use std::net::{SocketAddr, TcpStream};
7use std::time::Duration;
8use testcontainers::core::{ContainerAsync, IntoContainerPort};
9use testcontainers::{GenericImage, ImageExt, ReuseDirective, runners::AsyncRunner};
10
11pub struct SmtpTestContainer {
13 smtp_address: SocketAddr,
14 api_base_url: String,
15 client: reqwest::Client,
16 _container: ContainerAsync<GenericImage>,
17 _lease: ContainerLease,
18}
19
20impl SmtpTestContainer {
21 pub async fn start(suite: &TestContainerSuite) -> Self {
23 let lock = ContainerStateLock::acquire(suite, "mailpit");
24 let mut state = lock.load();
25 let _ = state.prune_stale();
26 state.register_pid(std::process::id());
27 lock.save(&state);
28 let container = GenericImage::new("axllent/mailpit", "v1.21.8")
29 .with_exposed_port(IntoContainerPort::tcp(1025))
30 .with_exposed_port(IntoContainerPort::tcp(8025))
31 .with_container_name(suite.container_name("mailpit"))
32 .with_reuse(ReuseDirective::Always)
33 .start()
34 .await
35 .expect("failed to start Mailpit test container");
36 let smtp_port = container
37 .get_host_port_ipv4(IntoContainerPort::tcp(1025))
38 .await
39 .expect("Mailpit SMTP port should be exposed");
40 let api_port = container
41 .get_host_port_ipv4(IntoContainerPort::tcp(8025))
42 .await
43 .expect("Mailpit API port should be exposed");
44 drop(lock);
45 let smtp_address = SocketAddr::from(([127, 0, 0, 1], smtp_port));
46 let smtp_ready = wait_until(
47 Duration::from_secs(90),
48 Duration::from_millis(250),
49 || async {
50 TcpStream::connect_timeout(&smtp_address, Duration::from_millis(500)).is_ok()
51 },
52 )
53 .await;
54 assert!(smtp_ready, "Mailpit SMTP endpoint did not become ready");
55
56 let api_base_url = format!("http://127.0.0.1:{api_port}");
57 let client = reqwest::Client::new();
58 let api_ready = wait_until(
59 Duration::from_secs(90),
60 Duration::from_millis(250),
61 || async {
62 client
63 .get(format!("{api_base_url}/api/v1/messages"))
64 .send()
65 .await
66 .is_ok_and(|response| response.status().is_success())
67 },
68 )
69 .await;
70 assert!(api_ready, "Mailpit API endpoint did not become ready");
71
72 Self {
73 smtp_address,
74 api_base_url,
75 client,
76 _container: container,
77 _lease: ContainerLease::new(suite.clone(), "mailpit"),
78 }
79 }
80
81 pub fn smtp_address(&self) -> SocketAddr {
83 self.smtp_address
84 }
85
86 pub async fn clear_messages(&self) {
88 let response = self
89 .client
90 .delete(format!("{}/api/v1/messages", self.api_base_url))
91 .send()
92 .await
93 .expect("failed to clear Mailpit messages");
94 assert!(
95 response.status().is_success(),
96 "Mailpit message cleanup failed with {}",
97 response.status()
98 );
99 }
100
101 pub async fn message_count(&self) -> u64 {
103 let response = self
104 .client
105 .get(format!("{}/api/v1/messages", self.api_base_url))
106 .send()
107 .await
108 .expect("failed to query Mailpit messages");
109 let status = response.status();
110 let body: serde_json::Value = response
111 .json()
112 .await
113 .expect("failed to decode Mailpit messages response");
114 assert!(status.is_success(), "Mailpit API failed: {body}");
115 body["total"]
116 .as_u64()
117 .expect("Mailpit response should include total")
118 }
119}