Samfredoly commited on
Commit
20fe538
·
verified ·
1 Parent(s): fa11f14

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -80
app.py CHANGED
@@ -10,7 +10,7 @@ from aiosmtpd.smtp import SMTP, AuthResult, LoginPassword
10
  from dotenv import load_dotenv
11
  import os
12
  import threading
13
- from typing import Optional, Tuple
14
 
15
  # Load environment variables
16
  load_dotenv()
@@ -30,7 +30,10 @@ SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "password")
30
  # WhatsApp webhook configuration
31
  WHATSAPP_WEBHOOK_TOKEN = os.getenv("WHATSAPP_WEBHOOK_TOKEN", "your_webhook_token")
32
  WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_phone_number_id")
33
- META_ACCESS_TOKEN = os.getenv("META_ACCESS_TOKEN", "default_meta_access_token") # For outgoing WhatsApp API calls
 
 
 
34
 
35
  # Authentication Credentials for SMTP server
36
  VALID_USERS = {
@@ -40,44 +43,16 @@ VALID_USERS = {
40
  # Flask app for WhatsApp bot
41
  app = Flask(__name__)
42
 
43
- # Regular expression for parsing the WhatsApp email command.
44
- # Expected format: "send email to <recipient> subject <subject> body <body>"
45
- EMAIL_COMMAND_PATTERN = re.compile(
46
- r"send email to\s+(?P<recipient>\S+)\s+subject\s+(?P<subject>.+?)\s+body\s+(?P<body>.+)$",
47
- re.IGNORECASE,
48
- )
49
 
50
  def validate_email(email: str) -> bool:
51
- """Basic email validation using a simple regex."""
52
- pattern = r"^[^@]+@[^@]+\.[^@]+$"
53
- return re.match(pattern, email) is not None
54
-
55
- def parse_message(message: str) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
56
- """
57
- Parse the incoming WhatsApp message.
58
- Returns a tuple: (recipient, subject, body, error_message)
59
- If parsing is successful, error_message will be None.
60
- """
61
- match = EMAIL_COMMAND_PATTERN.match(message)
62
- if not match:
63
- return None, None, None, ("Invalid format. Use: 'send email to <recipient> subject <subject> body <body>'.")
64
-
65
- recipient = match.group("recipient").strip()
66
- subject = match.group("subject").strip()
67
- body = match.group("body").strip()
68
-
69
- if not validate_email(recipient):
70
- return None, None, None, f"Invalid email address: {recipient}"
71
- if not subject:
72
- return None, None, None, "Subject cannot be empty."
73
- if not body:
74
- return None, None, None, "Email body cannot be empty."
75
-
76
- return recipient, subject, body, None
77
 
78
  class AuthenticatedSMTPHandler(Sink):
79
  """
80
- Custom SMTP handler for processing incoming emails with authentication.
81
  """
82
  async def handle_AUTH(self, server: SMTP, args: str, mechanism: str) -> Optional[AuthResult]:
83
  if mechanism.upper() != "LOGIN":
@@ -86,7 +61,6 @@ class AuthenticatedSMTPHandler(Sink):
86
 
87
  async def handle_AUTH_LOGIN(self, server: SMTP, args: str) -> Optional[AuthResult]:
88
  try:
89
- # Expecting args in the form "username password"
90
  parts = args.split()
91
  if len(parts) != 2:
92
  return AuthResult(success=False, message="Invalid authentication format")
@@ -114,12 +88,11 @@ class AuthenticatedSMTPHandler(Sink):
114
 
115
  def start_smtp_server() -> Controller:
116
  """
117
- Start the SMTP server with SSL/TLS and authentication.
118
- Returns the controller instance.
119
  """
120
  try:
121
  ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
122
- ssl_context.load_cert_chain("cert.pem", "key.pem") # Load your certificate and key
123
  logger.info("SSL context created successfully.")
124
  except Exception as e:
125
  logger.error(f"Failed to load SSL certificate and key: {e}")
@@ -144,33 +117,37 @@ def start_smtp_server() -> Controller:
144
 
145
  return controller
146
 
147
- def send_email(recipient: str, subject: str, body: str) -> bool:
148
  """
149
- Send an email using the SMTP client.
150
- Returns True if the email is sent successfully, otherwise False.
151
  """
152
  msg = MIMEText(body)
153
  msg['Subject'] = subject
154
- msg['From'] = SMTP_USER
155
- msg['To'] = recipient
156
 
157
  try:
158
  with smtplib.SMTP(CLIENT_SMTP_HOST, CLIENT_SMTP_PORT) as server:
159
- server.starttls() # Upgrade connection to TLS
160
  server.login(SMTP_USER, SMTP_PASSWORD)
161
- server.sendmail(SMTP_USER, [recipient], msg.as_string())
162
- logger.info(f"Email sent to {recipient}")
163
  return True
164
  except Exception as e:
165
- logger.error(f"Error sending email to {recipient}: {e}")
166
  return False
167
 
168
  @app.route("/webhook", methods=["GET", "POST"])
169
  def webhook():
170
  """
171
- Handle WhatsApp webhook verification and incoming messages.
172
- GET: Verify the webhook.
173
- POST: Process incoming messages and trigger email sending.
 
 
 
 
174
  """
175
  if request.method == "GET":
176
  mode = request.args.get("hub.mode")
@@ -185,57 +162,79 @@ def webhook():
185
  return "Verification failed", 403
186
 
187
  elif request.method == "POST":
188
- data = request.json
189
- logger.info(f"Incoming webhook data: {data}")
190
-
191
  try:
 
 
192
  value = data["entry"][0]["changes"][0]["value"]
193
  message = value["messages"][0]
194
-
195
- # Extract sender's WhatsApp number and phone number ID (fallback to env variable)
196
  sender_number = message.get("from", "unknown")
197
  phone_number_id = value.get("metadata", {}).get("phone_number_id", WHATSAPP_PHONE_NUMBER_ID)
198
- logger.info(f"Received message from {sender_number} using phone number ID: {phone_number_id}")
199
-
200
- message_body = message.get("text", {}).get("body", "")
201
- recipient, subject, body, error = parse_message(message_body)
202
-
203
- if error:
204
- response_msg = error
205
- logger.error(f"Message parsing error from {sender_number}: {error}")
206
- else:
207
- if send_email(recipient, subject, body):
208
- response_msg = f"Email sent to {recipient} with subject '{subject}'."
209
- else:
210
- response_msg = f"Failed to send email to {recipient}."
211
  except Exception as e:
212
- logger.error(f"Error processing message: {e}")
213
- response_msg = ("Error processing message. Please ensure the format is: "
214
- "'send email to <recipient> subject <subject> body <body>'.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- # If you need to send a response back to WhatsApp via the API, use META_ACCESS_TOKEN in the request header.
 
217
  return jsonify({"status": "success", "message": response_msg}), 200
218
 
219
  def run_flask_app():
220
- """
221
- Run the Flask app.
222
- """
223
  try:
224
  app.run(host="0.0.0.0", port=5000)
225
  except Exception as e:
226
  logger.error(f"Error starting Flask app: {e}")
227
 
228
  def main():
229
- """
230
- Main function to start the SMTP server and Flask app concurrently.
231
- """
232
  try:
233
  smtp_controller = start_smtp_server()
234
  except Exception as e:
235
  logger.critical("Failed to start SMTP server. Exiting.")
236
  return
237
 
238
- # Run Flask app in a separate thread
239
  flask_thread = threading.Thread(target=run_flask_app)
240
  flask_thread.start()
241
 
 
10
  from dotenv import load_dotenv
11
  import os
12
  import threading
13
+ from typing import Optional
14
 
15
  # Load environment variables
16
  load_dotenv()
 
30
  # WhatsApp webhook configuration
31
  WHATSAPP_WEBHOOK_TOKEN = os.getenv("WHATSAPP_WEBHOOK_TOKEN", "your_webhook_token")
32
  WHATSAPP_PHONE_NUMBER_ID = os.getenv("WHATSAPP_PHONE_NUMBER_ID", "default_phone_number_id")
33
+ META_ACCESS_TOKEN = os.getenv("META_ACCESS_TOKEN", "default_meta_access_token")
34
+
35
+ # Global conversation state mapping sender's WhatsApp number to their conversation details.
36
+ conversations = {}
37
 
38
  # Authentication Credentials for SMTP server
39
  VALID_USERS = {
 
43
  # Flask app for WhatsApp bot
44
  app = Flask(__name__)
45
 
46
+ # Regular expression for validating email addresses
47
+ EMAIL_REGEX = re.compile(r"^[^@]+@[^@]+\.[^@]+$")
 
 
 
 
48
 
49
  def validate_email(email: str) -> bool:
50
+ """Simple email validation using a regex."""
51
+ return re.match(EMAIL_REGEX, email) is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  class AuthenticatedSMTPHandler(Sink):
54
  """
55
+ Custom SMTP handler that logs incoming emails and enforces simple LOGIN authentication.
56
  """
57
  async def handle_AUTH(self, server: SMTP, args: str, mechanism: str) -> Optional[AuthResult]:
58
  if mechanism.upper() != "LOGIN":
 
61
 
62
  async def handle_AUTH_LOGIN(self, server: SMTP, args: str) -> Optional[AuthResult]:
63
  try:
 
64
  parts = args.split()
65
  if len(parts) != 2:
66
  return AuthResult(success=False, message="Invalid authentication format")
 
88
 
89
  def start_smtp_server() -> Controller:
90
  """
91
+ Starts the SMTP server with SSL/TLS and authentication.
 
92
  """
93
  try:
94
  ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
95
+ ssl_context.load_cert_chain("cert.pem", "key.pem")
96
  logger.info("SSL context created successfully.")
97
  except Exception as e:
98
  logger.error(f"Failed to load SSL certificate and key: {e}")
 
117
 
118
  return controller
119
 
120
+ def send_email(from_addr: str, to_addr: str, subject: str, body: str) -> bool:
121
  """
122
+ Sends an email using the SMTP client. The 'from_addr' is provided by the user.
123
+ Note: Some SMTP servers restrict the 'From' address to the authenticated user.
124
  """
125
  msg = MIMEText(body)
126
  msg['Subject'] = subject
127
+ msg['From'] = from_addr
128
+ msg['To'] = to_addr
129
 
130
  try:
131
  with smtplib.SMTP(CLIENT_SMTP_HOST, CLIENT_SMTP_PORT) as server:
132
+ server.starttls() # Upgrade to TLS
133
  server.login(SMTP_USER, SMTP_PASSWORD)
134
+ server.sendmail(from_addr, [to_addr], msg.as_string())
135
+ logger.info(f"Email sent from {from_addr} to {to_addr}")
136
  return True
137
  except Exception as e:
138
+ logger.error(f"Error sending email from {from_addr} to {to_addr}: {e}")
139
  return False
140
 
141
  @app.route("/webhook", methods=["GET", "POST"])
142
  def webhook():
143
  """
144
+ Handles WhatsApp webhook verification and processes incoming messages.
145
+ This bot acts as an SMTP assistant guiding the user to provide:
146
+ 1. 'From' email address
147
+ 2. Recipient ('To') email address
148
+ 3. Email subject
149
+ 4. Email body
150
+ Once all details are provided, the email is sent.
151
  """
152
  if request.method == "GET":
153
  mode = request.args.get("hub.mode")
 
162
  return "Verification failed", 403
163
 
164
  elif request.method == "POST":
 
 
 
165
  try:
166
+ data = request.json
167
+ logger.info(f"Incoming webhook data: {data}")
168
  value = data["entry"][0]["changes"][0]["value"]
169
  message = value["messages"][0]
 
 
170
  sender_number = message.get("from", "unknown")
171
  phone_number_id = value.get("metadata", {}).get("phone_number_id", WHATSAPP_PHONE_NUMBER_ID)
172
+ message_body = message.get("text", {}).get("body", "").strip()
 
 
 
 
 
 
 
 
 
 
 
 
173
  except Exception as e:
174
+ logger.error(f"Error parsing webhook data: {e}")
175
+ return jsonify({"status": "error", "message": "Invalid webhook payload"}), 400
176
+
177
+ # Retrieve or initialize the conversation state for this sender.
178
+ state = conversations.get(sender_number, {})
179
+
180
+ # If no conversation exists, start a new one.
181
+ if not state:
182
+ conversations[sender_number] = {"step": "from"}
183
+ response_msg = "Welcome to SMTP Assistant! Please provide your 'From' email address."
184
+ else:
185
+ step = state.get("step")
186
+ if step == "from":
187
+ if validate_email(message_body):
188
+ state["from"] = message_body
189
+ state["step"] = "to"
190
+ response_msg = "Thanks! Now, please provide the recipient's email address."
191
+ else:
192
+ response_msg = "Invalid email format. Please provide a valid 'From' email address."
193
+ elif step == "to":
194
+ if validate_email(message_body):
195
+ state["to"] = message_body
196
+ state["step"] = "subject"
197
+ response_msg = "Great! Please provide the email subject."
198
+ else:
199
+ response_msg = "Invalid email format. Please provide a valid recipient email address."
200
+ elif step == "subject":
201
+ state["subject"] = message_body
202
+ state["step"] = "body"
203
+ response_msg = "Almost done! Please provide the email body."
204
+ elif step == "body":
205
+ state["body"] = message_body
206
+ # All details collected; send the email.
207
+ from_addr = state.get("from")
208
+ to_addr = state.get("to")
209
+ subject = state.get("subject")
210
+ body = state.get("body")
211
+ if send_email(from_addr, to_addr, subject, body):
212
+ response_msg = f"Email sent successfully from {from_addr} to {to_addr}."
213
+ else:
214
+ response_msg = "Failed to send email. Please try again later."
215
+ # Clear conversation state.
216
+ del conversations[sender_number]
217
+ else:
218
+ response_msg = "Unrecognized step. Let's start over. Please provide your 'From' email address."
219
+ conversations[sender_number] = {"step": "from"}
220
 
221
+ # Optionally, if you wish to send a follow-up message via the WhatsApp Business API,
222
+ # you can use META_ACCESS_TOKEN in the HTTP request headers.
223
  return jsonify({"status": "success", "message": response_msg}), 200
224
 
225
  def run_flask_app():
 
 
 
226
  try:
227
  app.run(host="0.0.0.0", port=5000)
228
  except Exception as e:
229
  logger.error(f"Error starting Flask app: {e}")
230
 
231
  def main():
 
 
 
232
  try:
233
  smtp_controller = start_smtp_server()
234
  except Exception as e:
235
  logger.critical("Failed to start SMTP server. Exiting.")
236
  return
237
 
 
238
  flask_thread = threading.Thread(target=run_flask_app)
239
  flask_thread.start()
240