jebin2 commited on
Commit
2e708ff
·
1 Parent(s): 1273f44
.gitignore CHANGED
@@ -46,3 +46,4 @@ build/
46
 
47
  # Private keys (keep in repo but be careful)
48
  PRIVATE_KEY.pem
 
 
46
 
47
  # Private keys (keep in repo but be careful)
48
  PRIVATE_KEY.pem
49
+ client_secret.json
auth_utils.py CHANGED
@@ -25,6 +25,11 @@ if SMTP_SERVER == "127.0.0.1" and "gmail.com" in SMTP_USERNAME:
25
 
26
  SMTP_SENDER = os.getenv("SMTP_SENDER", SMTP_USERNAME)
27
 
 
 
 
 
 
28
  def verify_password(plain_password: str, hashed_password: str) -> bool:
29
  """
30
  Verify a password against a hash.
@@ -50,9 +55,16 @@ def generate_secret_key() -> str:
50
 
51
  def send_email(to_email: str, subject: str, body: str):
52
  """
53
- Send an email using SMTP (configured for ProtonMail Bridge).
54
  This function is blocking and should be run in a background task.
55
  """
 
 
 
 
 
 
 
56
  try:
57
  message = MIMEMultipart()
58
  message["From"] = SMTP_SENDER
@@ -63,9 +75,6 @@ def send_email(to_email: str, subject: str, body: str):
63
 
64
  # Create a secure SSL context
65
  context = ssl.create_default_context()
66
- # Note: ProtonMail Bridge with "change smtp-security" to SSL usually uses self-signed certs or specific setup.
67
- # If using localhost, we might need to bypass hostname check or trust the cert.
68
- # For now, we'll try standard SSL context. If it fails on localhost self-signed, we might need check_hostname=False.
69
  context.check_hostname = False
70
  context.verify_mode = ssl.CERT_NONE
71
 
@@ -73,7 +82,7 @@ def send_email(to_email: str, subject: str, body: str):
73
  server.login(SMTP_USERNAME, SMTP_PASSWORD)
74
  server.sendmail(SMTP_SENDER, to_email, message.as_string())
75
 
76
- logger.info(f"Email sent successfully to {to_email}")
77
  return True
78
  except Exception as e:
79
  logger.error(f"Failed to send email to {to_email}: {e}")
 
25
 
26
  SMTP_SENDER = os.getenv("SMTP_SENDER", SMTP_USERNAME)
27
 
28
+ from gmail_service import GmailService
29
+
30
+ # Initialize Gmail Service
31
+ gmail_service = GmailService()
32
+
33
  def verify_password(plain_password: str, hashed_password: str) -> bool:
34
  """
35
  Verify a password against a hash.
 
55
 
56
  def send_email(to_email: str, subject: str, body: str):
57
  """
58
+ Send an email using Gmail API.
59
  This function is blocking and should be run in a background task.
60
  """
61
+ # Try Gmail API first
62
+ if gmail_service.authenticate():
63
+ return gmail_service.send_email(to_email, subject, body)
64
+
65
+ logger.warning("Gmail API credentials not found or invalid. Falling back to SMTP (may fail on some platforms).")
66
+
67
+ # Fallback to SMTP (Original Implementation)
68
  try:
69
  message = MIMEMultipart()
70
  message["From"] = SMTP_SENDER
 
75
 
76
  # Create a secure SSL context
77
  context = ssl.create_default_context()
 
 
 
78
  context.check_hostname = False
79
  context.verify_mode = ssl.CERT_NONE
80
 
 
82
  server.login(SMTP_USERNAME, SMTP_PASSWORD)
83
  server.sendmail(SMTP_SENDER, to_email, message.as_string())
84
 
85
+ logger.info(f"Email sent successfully to {to_email} via SMTP")
86
  return True
87
  except Exception as e:
88
  logger.error(f"Failed to send email to {to_email}: {e}")
get_gmail_token.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ from google_auth_oauthlib.flow import InstalledAppFlow
4
+
5
+ # If modifying these scopes, delete the file token.json.
6
+ SCOPES = ['https://www.googleapis.com/auth/gmail.send']
7
+
8
+ def get_refresh_token():
9
+ """Shows basic usage of the Gmail API.
10
+ Lists the user's Gmail labels.
11
+ """
12
+ creds = None
13
+
14
+ # Check if client_secret.json exists
15
+ if not os.path.exists('client_secret.json'):
16
+ print("Error: 'client_secret.json' not found.")
17
+ print("Please download your OAuth 2.0 Client ID JSON file from Google Cloud Console,")
18
+ print("rename it to 'client_secret.json', and place it in this directory.")
19
+ return
20
+
21
+ flow = InstalledAppFlow.from_client_secrets_file(
22
+ 'client_secret.json', SCOPES)
23
+
24
+ # Run the local server flow to get credentials
25
+ creds = flow.run_local_server(port=0)
26
+
27
+ print("\n--- Authentication Successful ---")
28
+ print("Here are your credentials details. Please add these to your .env file or HF Secrets:\n")
29
+
30
+ # Extract client config to get ID and Secret easily if needed, though they are in the json file
31
+ with open('client_secret.json', 'r') as f:
32
+ client_config = json.load(f)
33
+ installed = client_config.get('installed', client_config.get('web', {}))
34
+ print(f"GOOGLE_CLIENT_ID={installed.get('client_id')}")
35
+ print(f"GOOGLE_CLIENT_SECRET={installed.get('client_secret')}")
36
+
37
+ print(f"GOOGLE_REFRESH_TOKEN={creds.refresh_token}")
38
+ print("\n---------------------------------")
39
+ print("Note: The access token will expire, but the refresh token is long-lived.")
40
+ print("Use the refresh token in your application to get new access tokens automatically.")
41
+
42
+ if __name__ == '__main__':
43
+ get_refresh_token()
gmail_service.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ import logging
4
+ from email.mime.text import MIMEText
5
+ from email.mime.multipart import MIMEMultipart
6
+ from google.auth.transport.requests import Request
7
+ from google.oauth2.credentials import Credentials
8
+ from google_auth_oauthlib.flow import InstalledAppFlow
9
+ from googleapiclient.discovery import build
10
+ from googleapiclient.errors import HttpError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class GmailService:
15
+ SCOPES = ['https://www.googleapis.com/auth/gmail.send']
16
+
17
+ def __init__(self):
18
+ self.creds = None
19
+ self.service = None
20
+ self.client_id = os.getenv('GOOGLE_CLIENT_ID')
21
+ self.client_secret = os.getenv('GOOGLE_CLIENT_SECRET')
22
+ self.refresh_token = os.getenv('GOOGLE_REFRESH_TOKEN')
23
+ self.sender_email = os.getenv('SMTP_SENDER') or os.getenv('EMAIL_ID')
24
+
25
+ def authenticate(self):
26
+ """Authenticate using the refresh token."""
27
+ if not all([self.client_id, self.client_secret, self.refresh_token]):
28
+ logger.error("Missing Google API credentials (CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN)")
29
+ return False
30
+
31
+ try:
32
+ # Create credentials object from the refresh token
33
+ self.creds = Credentials(
34
+ None, # No access token initially
35
+ refresh_token=self.refresh_token,
36
+ token_uri="https://oauth2.googleapis.com/token",
37
+ client_id=self.client_id,
38
+ client_secret=self.client_secret,
39
+ scopes=self.SCOPES
40
+ )
41
+
42
+ # Refresh the token (get a new access token)
43
+ if self.creds and self.creds.expired and self.creds.refresh_token:
44
+ self.creds.refresh(Request())
45
+
46
+ self.service = build('gmail', 'v1', credentials=self.creds)
47
+ return True
48
+ except Exception as e:
49
+ logger.error(f"Failed to authenticate with Gmail API: {e}")
50
+ return False
51
+
52
+ def send_email(self, to_email: str, subject: str, body: str) -> bool:
53
+ """Send an email using the Gmail API."""
54
+ if not self.service:
55
+ if not self.authenticate():
56
+ return False
57
+
58
+ try:
59
+ message = MIMEMultipart()
60
+ message['to'] = to_email
61
+ message['from'] = self.sender_email
62
+ message['subject'] = subject
63
+ message.attach(MIMEText(body, 'plain'))
64
+
65
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode('utf-8')
66
+ body = {'raw': raw_message}
67
+
68
+ message = (self.service.users().messages().send(userId="me", body=body).execute())
69
+ logger.info(f"Email sent to {to_email} (Message Id: {message['id']})")
70
+ return True
71
+
72
+ except HttpError as error:
73
+ logger.error(f"An error occurred sending email to {to_email}: {error}")
74
+ return False
75
+ except Exception as e:
76
+ logger.error(f"Unexpected error sending email: {e}")
77
+ return False
requirements.txt CHANGED
@@ -10,3 +10,6 @@ httpx>=0.25.0
10
  passlib[bcrypt]>=1.7.4
11
  email-validator>=2.0.0
12
  python-dotenv>=1.0.0
 
 
 
 
10
  passlib[bcrypt]>=1.7.4
11
  email-validator>=2.0.0
12
  python-dotenv>=1.0.0
13
+ google-api-python-client>=2.0.0
14
+ google-auth-oauthlib>=1.0.0
15
+ google-auth-httplib2>=0.1.0
test_gmail_service.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+ from gmail_service import GmailService
4
+
5
+ class TestGmailService(unittest.TestCase):
6
+
7
+ @patch('gmail_service.build')
8
+ @patch('gmail_service.Credentials')
9
+ def test_send_email_success(self, mock_credentials, mock_build):
10
+ # Mock the service and its methods
11
+ mock_service = MagicMock()
12
+ mock_build.return_value = mock_service
13
+
14
+ # Mock the send method
15
+ mock_messages = mock_service.users.return_value.messages.return_value
16
+ mock_messages.send.return_value.execute.return_value = {'id': '12345'}
17
+
18
+ # Initialize service
19
+ service = GmailService()
20
+ service.client_id = "dummy_id"
21
+ service.client_secret = "dummy_secret"
22
+ service.refresh_token = "dummy_token"
23
+
24
+ # Test authenticate
25
+ self.assertTrue(service.authenticate())
26
+
27
+ # Test send_email
28
+ result = service.send_email("test@example.com", "Test Subject", "Test Body")
29
+ self.assertTrue(result)
30
+
31
+ # Verify calls
32
+ mock_messages.send.assert_called_once()
33
+
34
+ def test_missing_credentials(self):
35
+ service = GmailService()
36
+ # Ensure credentials are None
37
+ service.client_id = None
38
+
39
+ self.assertFalse(service.authenticate())
40
+
41
+ if __name__ == '__main__':
42
+ unittest.main()