mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-10 13:05:56 +02:00
- Resizable folder sidebar with persisted width; nested folder tree with expand/collapse - Message search: scope all folders or single folder; listMessages honors UID filter - Drag-and-drop messages onto folders to move (multi-select supported) - SnappyMail import paths, folder settings store, wm DB migration and SQL install - IMAP quoted mailbox, IPv4 SMTP relay, compose recipient handling - Modal new/delete folder flows; dash-free UI copy; folder pills in search results
91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
import smtplib
|
|
import ssl
|
|
|
|
|
|
class SMTPClient:
|
|
"""Wrapper around smtplib.SMTP for sending mail via Postfix.
|
|
|
|
Supports two modes:
|
|
1. Authenticated (port 587 + STARTTLS) for standalone login sessions.
|
|
2. Local relay (port 25, no auth) for SSO sessions using master user.
|
|
Postfix accepts relay from localhost (permit_mynetworks in main.cf)
|
|
"""
|
|
|
|
def __init__(self, email_address, password, host='localhost', port=587,
|
|
use_local_relay=False):
|
|
self.email_address = email_address
|
|
self.password = password
|
|
# Postfix on AlmaLinux/CyberPanel often sets mynetworks=127.0.0.0/8 only.
|
|
# Python resolves "localhost" to ::1 first → SMTP is not treated as mynetworks
|
|
# → 554 Relay access denied. Force IPv4 loopback for predictable relay.
|
|
self.host = self._smtp_host_ipv4_loopback(host)
|
|
self.port = port
|
|
self.use_local_relay = use_local_relay
|
|
|
|
@staticmethod
|
|
def _smtp_host_ipv4_loopback(host):
|
|
if not host:
|
|
return '127.0.0.1'
|
|
h = str(host).strip().lower()
|
|
if h in ('localhost', '::1', '[::1]'):
|
|
return '127.0.0.1'
|
|
return host
|
|
|
|
def send_message(self, mime_message):
|
|
"""Send a composed email via SMTP.
|
|
|
|
Returns:
|
|
dict: {success: bool, message_id: str or None, error: str or None}
|
|
"""
|
|
try:
|
|
# Bind outbound socket to IPv4 so Postfix sees 127.0.0.1 (mynetworks), not ::1.
|
|
src = ('127.0.0.1', 0)
|
|
if self.use_local_relay:
|
|
# SSO mode: send via port 25 without auth
|
|
# Postfix permits relay from localhost (permit_mynetworks)
|
|
smtp = smtplib.SMTP(self.host, 25, source_address=src)
|
|
smtp.ehlo()
|
|
smtp.send_message(mime_message)
|
|
smtp.quit()
|
|
else:
|
|
# Standalone mode: authenticated via port 587 + STARTTLS
|
|
ctx = ssl.create_default_context()
|
|
ctx.check_hostname = False
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
|
|
smtp = smtplib.SMTP(self.host, self.port, source_address=src)
|
|
smtp.ehlo()
|
|
smtp.starttls(context=ctx)
|
|
smtp.ehlo()
|
|
smtp.login(self.email_address, self.password)
|
|
smtp.send_message(mime_message)
|
|
smtp.quit()
|
|
|
|
message_id = mime_message.get('Message-ID', '')
|
|
return {'success': True, 'message_id': message_id}
|
|
except smtplib.SMTPAuthenticationError as e:
|
|
return {'success': False, 'message_id': None, 'error': 'Authentication failed: %s' % str(e)}
|
|
except smtplib.SMTPRecipientsRefused as e:
|
|
return {'success': False, 'message_id': None, 'error': 'Recipients refused: %s' % str(e)}
|
|
except Exception as e:
|
|
return {'success': False, 'message_id': None, 'error': str(e)}
|
|
|
|
def save_to_sent(self, imap_client, raw_message):
|
|
"""Append sent message to the Sent folder via IMAP.
|
|
|
|
CyberPanel's Dovecot uses INBOX.Sent as the Sent folder.
|
|
"""
|
|
# Try CyberPanel's actual folder name first, then fallbacks
|
|
sent_folders = ['INBOX.Sent', 'Sent', 'Sent Messages', 'Sent Items']
|
|
for folder in sent_folders:
|
|
try:
|
|
if imap_client.append_message(folder, raw_message, '\\Seen'):
|
|
return True
|
|
except Exception:
|
|
continue
|
|
try:
|
|
imap_client.create_folder('INBOX.Sent')
|
|
return imap_client.append_message('INBOX.Sent', raw_message, '\\Seen')
|
|
except Exception:
|
|
return False
|