Update mail2sms/main.py
Build and Push Docker Image / test-and-build (push) Successful in 44s

Cette révision appartient à :
2026-02-26 05:40:29 +00:00
Parent 206757b5db
révision bb078a37e7
+256 -299
Voir le fichier
@@ -1,300 +1,257 @@
import os
import time
import json
import re
import socket
import requests
import smtplib
from email.message import EmailMessage
from imapclient import IMAPClient
from requests.auth import HTTPBasicAuth
from mail2sms.text_utils import html_to_sms_text
# ============================================================
# CONFIGURATION
# ============================================================
IMAP_SERVER = os.getenv("IMAP_SERVER")
IMAP_PORT = int(os.getenv("IMAP_PORT", 993))
EMAIL_ACCOUNT = os.getenv("EMAIL_ACCOUNT")
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")
SMTP_SERVER = os.getenv("SMTP_SERVER")
SMTP_PORT = int(os.getenv("SMTP_PORT", 587))
API_URL = os.getenv("API_URL")
API_USERNAME = os.getenv("API_USERNAME")
API_PASSWORD = os.getenv("API_PASSWORD")
DEFAULT_DEVICE_ID = os.getenv("DEFAULT_DEVICE_ID")
DEFAULT_SIM_NUMBER = os.getenv("DEFAULT_SIM_NUMBER")
IDLE_TIMEOUT = 60 * 25 # 25 minutes (safe before typical 30 min timeout)
TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true"
# ============================================================
# LOGGING
# ============================================================
def log(message):
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}", flush=True)
def html_to_sms_text(raw_html):
"""
Convert HTML content to clean SMS-compatible plain text.
- Removes HTML tags
- Decodes HTML entities
- Normalizes unicode
- Removes unsupported SMS characters
"""
if not raw_html:
return ""
# Parse HTML
soup = BeautifulSoup(raw_html, "html.parser")
# Get text only
text = soup.get_text(separator=" ")
# Decode HTML entities (  & etc.)
text = html.unescape(text)
# Normalize unicode (remove weird accents/combined chars)
text = unicodedata.normalize("NFKD", text)
# Remove non GSM basic characters
gsm_basic = (
"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ"
+ ' !"%&\'()*+,-./'
+ "0123456789:;<=>?"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "ÄÖÑܧ¿"
+ "abcdefghijklmnopqrstuvwxyz"
+ "äöñüà"
)
cleaned = "".join(c for c in text if c in gsm_basic)
# Remove multiple spaces
cleaned = re.sub(r"\s+", " ", cleaned).strip()
return cleaned
# ============================================================
# SUBJECT PARAMETER EXTRACTION
# ============================================================
def extract_parameters(subject):
"""
Extract parameters from email subject.
Returns: to, device_id, sim_number, api_login, api_password
"""
if not subject:
return None, None, None, None, None
# Regex case-insensitive
def get_param(name):
match = re.search(rf"{name}\s*=\s*([^; ]+)", subject, re.IGNORECASE)
return match.group(1) if match else None
to = get_param("to")
device_id = get_param("deviceId")
sim_number = get_param("simNumber")
api_login = get_param("login")
api_password = get_param("password")
return to, device_id, sim_number, api_login, api_password
# ============================================================
# SMS SEND
# ============================================================
def send_sms(payload, username, password):
"""
Send SMS using the API.
In TEST_MODE, it returns a fake response.
If username/password are provided, they override the defaults from env.
"""
if TEST_MODE:
log("TEST_MODE enabled - Fake SMS send")
class FakeResponse:
status_code = 202
text = json.dumps({"id": "fake-id", "state": "Sent"})
return FakeResponse()
return requests.post(
API_URL,
auth=HTTPBasicAuth(username, password),
headers={"Content-Type": "application/json"},
data=json.dumps(payload),
timeout=15
)
# ============================================================
# STATUS EMAIL
# ============================================================
def send_status_email(to_address, phone, status_code, response_text):
if TEST_MODE:
log("TEST_MODE enabled - Fake email send")
return
msg = EmailMessage()
msg["From"] = EMAIL_ACCOUNT
msg["To"] = to_address
msg["Subject"] = f"SMS Status for {phone}"
msg.set_content(
f"Destination: {phone}\n"
f"HTTP Code: {status_code}\n\n"
f"API Response:\n{response_text}"
)
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
server.starttls()
server.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
server.send_message(msg)
log(f"Status email sent to {to_address}")
except Exception as e:
log(f"SMTP error: {e}")
# ============================================================
# PROCESS EMAILS READING
# ============================================================
def process_unseen(server):
messages = server.search(['UNSEEN'])
if not messages:
return
log(f"{len(messages)} new message(s) detected")
fetched = server.fetch(messages, ['RFC822'])
for uid, message_data in fetched.items():
try:
import email
msg = email.message_from_bytes(message_data[b'RFC822'])
sender = email.utils.parseaddr(msg["From"])[1]
subject = msg["Subject"]
phone_number, subject_device_id, subject_sim_number, api_login, api_password = extract_parameters(subject)
if not phone_number:
log("No phone number found in subject")
server.add_flags(uid, ['\\Seen'])
continue
device_id = subject_device_id or DEFAULT_DEVICE_ID
sim_number = subject_sim_number or DEFAULT_SIM_NUMBER
username = api_login or API_USERNAME
if not username:
log("No API Username found")
server.add_flags(uid, ['\\Seen'])
continue
password = api_password or API_PASSWORD
if not password:
log("No API Password found")
server.add_flags(uid, ['\\Seen'])
continue
sms_text = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
payload = part.get_payload(decode=True)
if not payload:
continue
decoded = payload.decode(errors="ignore")
if content_type == "text/plain":
sms_text = decoded.strip()
break
elif content_type == "text/html" and not sms_text:
sms_text = html_to_sms_text(decoded)
else:
payload = msg.get_payload(decode=True)
if payload:
decoded = payload.decode(errors="ignore")
if msg.get_content_type() == "text/html":
sms_text = html_to_sms_text(decoded)
else:
sms_text = decoded.strip()
payload = {
"textMessage": {"text": sms_text},
"phoneNumbers": [phone_number]
}
if device_id:
payload["deviceId"] = device_id
if sim_number:
payload["simNumber"] = int(sim_number)
log(f"Sending SMS to {phone_number} using deviceId={device_id} simNumber={sim_number}")
response = send_sms(payload, username, password)
log(f"HTTP {response.status_code}")
send_status_email(sender, phone_number, response.status_code, response.text)
if 200 <= response.status_code <= 204:
server.delete_messages(uid)
log("Email deleted (success)")
else:
server.add_flags(uid, ['\\Seen'])
log("Email kept (non-success response)")
except Exception as e:
log(f"Processing error: {e}")
server.add_flags(uid, ['\\Seen'])
server.expunge()
# ============================================================
# HANDLE IMAP CONNECTION
# ============================================================
def connect():
while True:
try:
log("Connecting to IMAP server...")
server = IMAPClient(IMAP_SERVER, port=IMAP_PORT, ssl=True, timeout=20)
server.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
server.select_folder("INBOX")
log("IMAP connected successfully")
return server
except (socket.error, Exception) as e:
log(f"IMAP connection failed: {e}")
time.sleep(10)
# ============================================================
# IDLE LOOP
# ============================================================
def idle_loop():
server = connect()
while True:
try:
log("Entering IDLE mode")
server.idle()
responses = server.idle_check(timeout=IDLE_TIMEOUT)
server.idle_done()
if responses:
log("New mail event received")
process_unseen(server)
else:
log("Idle timeout reached (keep-alive)")
except (socket.error, Exception) as e:
log(f"Connection lost: {e}")
try:
server.logout()
except:
pass
time.sleep(5)
server = connect()
# ============================================================
# ENTRY POINT
# ============================================================
if __name__ == "__main__":
log("Mail2SMS service started")
if TEST_MODE:
log("Running in TEST_MODE - no IMAP connection")
else:
import os
import time
import json
import re
import socket
import requests
import smtplib
from email.message import EmailMessage
from imapclient import IMAPClient
from requests.auth import HTTPBasicAuth
from mail2sms.text_utils import html_to_sms_text
# ============================================================
# CONFIGURATION
# ============================================================
IMAP_SERVER = os.getenv("IMAP_SERVER")
IMAP_PORT = int(os.getenv("IMAP_PORT", 993))
EMAIL_ACCOUNT = os.getenv("EMAIL_ACCOUNT")
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")
SMTP_SERVER = os.getenv("SMTP_SERVER")
SMTP_PORT = int(os.getenv("SMTP_PORT", 587))
API_URL = os.getenv("API_URL")
API_USERNAME = os.getenv("API_USERNAME")
API_PASSWORD = os.getenv("API_PASSWORD")
DEFAULT_DEVICE_ID = os.getenv("DEFAULT_DEVICE_ID")
DEFAULT_SIM_NUMBER = os.getenv("DEFAULT_SIM_NUMBER")
IDLE_TIMEOUT = 60 * 25 # 25 minutes (safe before typical 30 min timeout)
TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true"
# ============================================================
# LOGGING
# ============================================================
def log(message):
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {message}", flush=True)
# ============================================================
# SUBJECT PARAMETER EXTRACTION
# ============================================================
def extract_parameters(subject):
"""
Extract parameters from email subject.
Returns: to, device_id, sim_number, api_login, api_password
"""
if not subject:
return None, None, None, None, None
# Regex case-insensitive
def get_param(name):
match = re.search(rf"{name}\s*=\s*([^; ]+)", subject, re.IGNORECASE)
return match.group(1) if match else None
to = get_param("to")
device_id = get_param("deviceId")
sim_number = get_param("simNumber")
api_login = get_param("login")
api_password = get_param("password")
return to, device_id, sim_number, api_login, api_password
# ============================================================
# SMS SEND
# ============================================================
def send_sms(payload, username, password):
"""
Send SMS using the API.
In TEST_MODE, it returns a fake response.
If username/password are provided, they override the defaults from env.
"""
if TEST_MODE:
log("TEST_MODE enabled - Fake SMS send")
class FakeResponse:
status_code = 202
text = json.dumps({"id": "fake-id", "state": "Sent"})
return FakeResponse()
return requests.post(
API_URL,
auth=HTTPBasicAuth(username, password),
headers={"Content-Type": "application/json"},
data=json.dumps(payload),
timeout=15
)
# ============================================================
# STATUS EMAIL
# ============================================================
def send_status_email(to_address, phone, status_code, response_text):
if TEST_MODE:
log("TEST_MODE enabled - Fake email send")
return
msg = EmailMessage()
msg["From"] = EMAIL_ACCOUNT
msg["To"] = to_address
msg["Subject"] = f"SMS Status for {phone}"
msg.set_content(
f"Destination: {phone}\n"
f"HTTP Code: {status_code}\n\n"
f"API Response:\n{response_text}"
)
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10) as server:
server.starttls()
server.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
server.send_message(msg)
log(f"Status email sent to {to_address}")
except Exception as e:
log(f"SMTP error: {e}")
# ============================================================
# PROCESS EMAILS READING
# ============================================================
def process_unseen(server):
messages = server.search(['UNSEEN'])
if not messages:
return
log(f"{len(messages)} new message(s) detected")
fetched = server.fetch(messages, ['RFC822'])
for uid, message_data in fetched.items():
try:
import email
msg = email.message_from_bytes(message_data[b'RFC822'])
sender = email.utils.parseaddr(msg["From"])[1]
subject = msg["Subject"]
phone_number, subject_device_id, subject_sim_number, api_login, api_password = extract_parameters(subject)
if not phone_number:
log("No phone number found in subject")
server.add_flags(uid, ['\\Seen'])
continue
device_id = subject_device_id or DEFAULT_DEVICE_ID
sim_number = subject_sim_number or DEFAULT_SIM_NUMBER
username = api_login or API_USERNAME
if not username:
log("No API Username found")
server.add_flags(uid, ['\\Seen'])
continue
password = api_password or API_PASSWORD
if not password:
log("No API Password found")
server.add_flags(uid, ['\\Seen'])
continue
sms_text = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
payload = part.get_payload(decode=True)
if not payload:
continue
decoded = payload.decode(errors="ignore")
if content_type == "text/plain":
sms_text = decoded.strip()
break
elif content_type == "text/html" and not sms_text:
sms_text = html_to_sms_text(decoded)
else:
payload = msg.get_payload(decode=True)
if payload:
decoded = payload.decode(errors="ignore")
if msg.get_content_type() == "text/html":
sms_text = html_to_sms_text(decoded)
else:
sms_text = decoded.strip()
payload = {
"textMessage": {"text": sms_text},
"phoneNumbers": [phone_number]
}
if device_id:
payload["deviceId"] = device_id
if sim_number:
payload["simNumber"] = int(sim_number)
log(f"Sending SMS to {phone_number} using deviceId={device_id} simNumber={sim_number}")
response = send_sms(payload, username, password)
log(f"HTTP {response.status_code}")
send_status_email(sender, phone_number, response.status_code, response.text)
if 200 <= response.status_code <= 204:
server.delete_messages(uid)
log("Email deleted (success)")
else:
server.add_flags(uid, ['\\Seen'])
log("Email kept (non-success response)")
except Exception as e:
log(f"Processing error: {e}")
server.add_flags(uid, ['\\Seen'])
server.expunge()
# ============================================================
# HANDLE IMAP CONNECTION
# ============================================================
def connect():
while True:
try:
log("Connecting to IMAP server...")
server = IMAPClient(IMAP_SERVER, port=IMAP_PORT, ssl=True, timeout=20)
server.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
server.select_folder("INBOX")
log("IMAP connected successfully")
return server
except (socket.error, Exception) as e:
log(f"IMAP connection failed: {e}")
time.sleep(10)
# ============================================================
# IDLE LOOP
# ============================================================
def idle_loop():
server = connect()
while True:
try:
log("Entering IDLE mode")
server.idle()
responses = server.idle_check(timeout=IDLE_TIMEOUT)
server.idle_done()
if responses:
log("New mail event received")
process_unseen(server)
else:
log("Idle timeout reached (keep-alive)")
except (socket.error, Exception) as e:
log(f"Connection lost: {e}")
try:
server.logout()
except:
pass
time.sleep(5)
server = connect()
# ============================================================
# ENTRY POINT
# ============================================================
if __name__ == "__main__":
log("Mail2SMS service started")
if TEST_MODE:
log("Running in TEST_MODE - no IMAP connection")
else:
idle_loop()