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 {
28 let lock = ContainerStateLock::acquire(suite, "mailpit");
29 let mut state = lock.load();
30 let _ = state.prune_stale_before_current_execution();
31 state.register_current_process();
32 lock.save(&state);
33 let container = GenericImage::new("axllent/mailpit", "v1.21.8")
34 .with_exposed_port(IntoContainerPort::tcp(1025))
35 .with_exposed_port(IntoContainerPort::tcp(8025))
36 .with_container_name(suite.container_name("mailpit"))
37 .with_reuse(ReuseDirective::Always)
38 .start()
39 .await
40 .expect("failed to start Mailpit test container");
41 let smtp_port = container
42 .get_host_port_ipv4(IntoContainerPort::tcp(1025))
43 .await
44 .expect("Mailpit SMTP port should be exposed");
45 let api_port = container
46 .get_host_port_ipv4(IntoContainerPort::tcp(8025))
47 .await
48 .expect("Mailpit API port should be exposed");
49 let smtp_address = SocketAddr::from(([127, 0, 0, 1], smtp_port));
50 let smtp_ready = wait_until(
51 Duration::from_secs(90),
52 Duration::from_millis(250),
53 || async {
54 TcpStream::connect_timeout(&smtp_address, Duration::from_millis(500)).is_ok()
55 },
56 )
57 .await;
58 assert!(smtp_ready, "Mailpit SMTP endpoint did not become ready");
59
60 let api_base_url = format!("http://127.0.0.1:{api_port}");
61 let client = reqwest::Client::new();
62 let api_ready = wait_until(
63 Duration::from_secs(90),
64 Duration::from_millis(250),
65 || async {
66 client
67 .get(format!("{api_base_url}/api/v1/messages"))
68 .send()
69 .await
70 .is_ok_and(|response| response.status().is_success())
71 },
72 )
73 .await;
74 assert!(api_ready, "Mailpit API endpoint did not become ready");
75 drop(lock);
76
77 Self {
78 smtp_address,
79 api_base_url,
80 client,
81 _container: container,
82 _lease: ContainerLease::new(suite.clone(), "mailpit"),
83 }
84 }
85
86 #[must_use]
88 pub fn smtp_address(&self) -> SocketAddr {
89 self.smtp_address
90 }
91
92 pub async fn clear_messages(&self) {
98 let response = self
99 .client
100 .delete(format!("{}/api/v1/messages", self.api_base_url))
101 .send()
102 .await
103 .expect("failed to clear Mailpit messages");
104 assert!(
105 response.status().is_success(),
106 "Mailpit message cleanup failed with {}",
107 response.status()
108 );
109 }
110
111 pub async fn message_count(&self) -> u64 {
118 let response = self
119 .client
120 .get(format!("{}/api/v1/messages", self.api_base_url))
121 .send()
122 .await
123 .expect("failed to query Mailpit messages");
124 let status = response.status();
125 let body: serde_json::Value = response
126 .json()
127 .await
128 .expect("failed to decode Mailpit messages response");
129 assert!(status.is_success(), "Mailpit API failed: {body}");
130 body["total"]
131 .as_u64()
132 .expect("Mailpit response should include total")
133 }
134}