1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
//! External connectivity detection for email server accessibility
use crate::error::{SynapseError, Result};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use tokio::net::{TcpListener, UdpSocket};
use tokio::time::timeout;
use tracing::{info, warn, debug};
/// Connectivity assessment results
#[derive(Debug, Clone)]
pub struct ConnectivityAssessment {
/// Can bind to SMTP port (25, 587, 2525)
pub can_bind_smtp: bool,
/// Can bind to IMAP port (143, 993, 1143)
pub can_bind_imap: bool,
/// Has external IP address
pub has_external_ip: bool,
/// External IP address if available
pub external_ip: Option<IpAddr>,
/// Firewall/NAT detection results
pub firewall_status: FirewallStatus,
/// Recommended server configuration
pub recommended_config: ServerRecommendation,
}
#[derive(Debug, Clone)]
pub enum FirewallStatus {
/// Ports are open and accessible from internet
Open,
/// Behind NAT/firewall, ports not accessible
Blocked,
/// Unable to determine (network issues)
Unknown,
}
#[derive(Debug, Clone)]
pub enum ServerRecommendation {
/// Run local email server (ports open, external IP)
RunLocalServer {
smtp_port: u16,
imap_port: u16,
external_ip: IpAddr,
},
/// Use relay-only mode (can send but not receive)
RelayOnly {
reason: String,
},
/// Use external email provider
ExternalProvider {
reason: String,
},
}
/// External connectivity detector
pub struct ConnectivityDetector {
/// Timeout for connectivity tests
test_timeout: Duration,
/// Ports to test for availability
smtp_ports: Vec<u16>,
imap_ports: Vec<u16>,
}
impl Default for ConnectivityDetector {
fn default() -> Self {
Self {
test_timeout: Duration::from_secs(10),
smtp_ports: vec![25, 587, 2525], // Standard, submission, alternative
imap_ports: vec![143, 993, 1143], // Standard, SSL, alternative
}
}
}
impl ConnectivityDetector {
/// Perform comprehensive connectivity assessment
pub async fn assess_connectivity(&self) -> Result<ConnectivityAssessment> {
info!("Starting connectivity assessment for email server...");
let can_bind_smtp = self.test_port_binding(&self.smtp_ports).await?;
let can_bind_imap = self.test_port_binding(&self.imap_ports).await?;
let external_ip = self.detect_external_ip().await;
let has_external_ip = external_ip.is_some();
let firewall_status = if has_external_ip && (can_bind_smtp || can_bind_imap) {
self.test_external_accessibility().await
} else {
FirewallStatus::Blocked
};
let recommended_config = self.determine_recommendation(
can_bind_smtp,
can_bind_imap,
&external_ip,
&firewall_status,
);
let assessment = ConnectivityAssessment {
can_bind_smtp,
can_bind_imap,
has_external_ip,
external_ip,
firewall_status,
recommended_config,
};
info!("Connectivity assessment completed: {:?}", assessment);
Ok(assessment)
}
/// Test if we can bind to email server ports
async fn test_port_binding(&self, ports: &[u16]) -> Result<bool> {
for &port in ports {
debug!("Testing port binding on port {}", port);
match timeout(self.test_timeout, TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), port))).await {
Ok(Ok(_listener)) => {
info!("Successfully bound to port {}", port);
return Ok(true);
}
Ok(Err(e)) => {
debug!("Failed to bind to port {}: {}", port, e);
}
Err(_) => {
debug!("Timeout binding to port {}", port);
}
}
}
warn!("Unable to bind to any ports in {:?}", ports);
Ok(false)
}
/// Detect external IP address
async fn detect_external_ip(&self) -> Option<IpAddr> {
debug!("Detecting external IP address...");
// Method 1: Try to connect to external services and check local address
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await {
if let Ok(_) = socket.connect("8.8.8.8:53").await {
if let Ok(local_addr) = socket.local_addr() {
let ip = local_addr.ip();
if !ip.is_loopback() && !self.is_private_ip(&ip) {
info!("Detected external IP via UDP: {}", ip);
return Some(ip);
}
}
}
}
// Method 2: Check for common public IP patterns
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await {
if let Ok(_) = socket.connect("1.1.1.1:53").await {
if let Ok(local_addr) = socket.local_addr() {
let ip = local_addr.ip();
if !ip.is_loopback() && !self.is_private_ip(&ip) {
info!("Detected external IP via UDP (method 2): {}", ip);
return Some(ip);
}
}
}
}
warn!("Unable to detect external IP address");
None
}
/// Check if an IP address is private/local
fn is_private_ip(&self, ip: &IpAddr) -> bool {
match ip {
IpAddr::V4(ipv4) => {
ipv4.is_private() || ipv4.is_loopback() || ipv4.is_link_local()
}
IpAddr::V6(ipv6) => {
ipv6.is_loopback() || ipv6.is_multicast()
}
}
}
/// Test external accessibility of ports
async fn test_external_accessibility(&self) -> FirewallStatus {
debug!("Testing external accessibility...");
// For development purposes, we'll assume firewall is blocking unless we can prove otherwise
// In production, this would use external port checking services or UPnP discovery
// Check if we can bind to privileged ports (indication of admin access)
if let Ok(_) = timeout(Duration::from_secs(1), TcpListener::bind("127.0.0.1:25")).await {
// If we can bind to port 25, we might be running as admin and ports could be open
return FirewallStatus::Open;
}
FirewallStatus::Unknown
}
/// Determine server configuration recommendation
fn determine_recommendation(
&self,
can_bind_smtp: bool,
can_bind_imap: bool,
external_ip: &Option<IpAddr>,
firewall_status: &FirewallStatus,
) -> ServerRecommendation {
match (can_bind_smtp, can_bind_imap, external_ip, firewall_status) {
// Ideal case: can bind to ports, have external IP, ports are open
(true, true, Some(ip), FirewallStatus::Open) => {
ServerRecommendation::RunLocalServer {
smtp_port: 2525, // Use non-privileged port
imap_port: 1143,
external_ip: *ip,
}
}
// Can bind to SMTP only
(true, false, Some(ip), FirewallStatus::Open) => {
ServerRecommendation::RunLocalServer {
smtp_port: 2525,
imap_port: 1143, // Will try to bind anyway
external_ip: *ip,
}
}
// Can bind but behind firewall/NAT
(true, _, _, FirewallStatus::Blocked) => {
ServerRecommendation::RelayOnly {
reason: "Ports available locally but blocked by firewall/NAT".to_string(),
}
}
// Can't bind to ports (likely running as non-root)
(false, false, _, _) => {
ServerRecommendation::ExternalProvider {
reason: "Unable to bind to email server ports (try running as administrator)".to_string(),
}
}
// No external IP
(_, _, None, _) => {
ServerRecommendation::RelayOnly {
reason: "No external IP address detected".to_string(),
}
}
// Unknown firewall status - be conservative but allow local use
(true, _, _, FirewallStatus::Unknown) => {
ServerRecommendation::RelayOnly {
reason: "Unable to determine external accessibility - running in local mode".to_string(),
}
}
// Can bind to IMAP only but not SMTP
(false, true, Some(_), _) => {
ServerRecommendation::ExternalProvider {
reason: "Can bind to IMAP but not SMTP - consider using external provider".to_string(),
}
}
}
}
/// Test specific port accessibility from external network
pub async fn test_external_port(&self, port: u16) -> Result<bool> {
debug!("Testing external accessibility of port {}", port);
// Bind to the port
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), port);
let _listener = timeout(self.test_timeout, TcpListener::bind(addr)).await
.map_err(|_| SynapseError::NetworkError("Timeout binding to port".to_string()))?
.map_err(|e| SynapseError::NetworkError(format!("Failed to bind to port {}: {}", port, e)))?;
// In production, this would test external connectivity via UPnP or external services
Ok(false) // Conservative default
}
}