Cette révision appartient à :
+256
-299
@@ -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()
|
||||
Référencer dans un nouveau ticket
Bloquer un utilisateur