KyrosDev commited on
Commit
a153e61
·
1 Parent(s): 2437f55

改用SendGrid API發送郵件

Browse files
Files changed (2) hide show
  1. .env.example +7 -7
  2. app/services/email_service.py +55 -35
.env.example CHANGED
@@ -18,15 +18,15 @@ APP_ENV=production
18
  # API 設定
19
  API_PREFIX=/api
20
 
21
- # ==================== 郵件服務設定 (Resend API) ====================
22
- # 透過 Resend API 發送郵件
23
- # 註冊: https://resend.com
24
 
25
- # Resend API Key (Private Secret)
26
- RESEND_API_KEY=re_xxxxxxxxxx
27
 
28
- # 寄件人郵箱 (需在 Resend 驗證網域或使預設 onboarding@resend.dev)
29
- RESEND_FROM_EMAIL=onboarding@resend.dev
30
 
31
  # 寄件人名稱
32
  FROM_NAME=KSTools
 
18
  # API 設定
19
  API_PREFIX=/api
20
 
21
+ # ==================== 郵件服務設定 (SendGrid API) ====================
22
+ # 透過 SendGrid API 發送郵件
23
+ # 註冊: https://sendgrid.com (免費 100封/天)
24
 
25
+ # SendGrid API Key (Private Secret)
26
+ SENDGRID_API_KEY=SG.xxxxxxxxxx
27
 
28
+ # 寄件人郵箱 (需在 SendGrid 驗證,個人 Gmail)
29
+ SENDGRID_FROM_EMAIL=your-email@gmail.com
30
 
31
  # 寄件人名稱
32
  FROM_NAME=KSTools
app/services/email_service.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  KSTools 郵件服務
3
- 使用 Resend API 發送版本更新通知郵件
4
  """
5
 
6
  import os
@@ -18,18 +18,18 @@ logger = logging.getLogger(__name__)
18
  class EmailService:
19
  """
20
  郵件服務類別
21
- 透過 Resend API 發送郵件
22
  """
23
 
24
  def __init__(self):
25
  """初始化郵件服務"""
26
- self.api_key = os.environ.get('RESEND_API_KEY', '')
27
- self.from_email = os.environ.get('RESEND_FROM_EMAIL', 'onboarding@resend.dev')
28
  self.from_name = os.environ.get('FROM_NAME', 'KSTools')
29
- self.api_url = 'https://api.resend.com/emails'
30
 
31
  if not self.api_key:
32
- logger.warning("RESEND_API_KEY not configured")
33
 
34
  def is_configured(self) -> bool:
35
  """檢查郵件服務是否已設定"""
@@ -112,9 +112,9 @@ class EmailService:
112
  </html>
113
  """
114
 
115
- async def _send_via_resend(self, to_email: str, subject: str, html_content: str) -> dict:
116
  """
117
- 透過 Resend API 發送單封郵件
118
 
119
  Args:
120
  to_email: 收件人郵箱
@@ -125,7 +125,24 @@ class EmailService:
125
  API 回應
126
  """
127
  try:
128
- from_address = f"{self.from_name} <{self.from_email}>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  async with httpx.AsyncClient() as client:
131
  response = await client.post(
@@ -134,23 +151,19 @@ class EmailService:
134
  'Authorization': f'Bearer {self.api_key}',
135
  'Content-Type': 'application/json'
136
  },
137
- json={
138
- 'from': from_address,
139
- 'to': [to_email],
140
- 'subject': subject,
141
- 'html': html_content
142
- },
143
  timeout=30
144
  )
145
 
146
- if response.status_code == 200:
147
- return {'success': True, 'data': response.json()}
 
148
  else:
149
- logger.error(f"Resend API error: {response.status_code} - {response.text}")
150
  return {'success': False, 'error': response.text}
151
 
152
  except Exception as e:
153
- logger.error(f"Resend API call failed: {e}")
154
  return {'success': False, 'error': str(e)}
155
 
156
  async def send_version_announcement(
@@ -163,7 +176,7 @@ class EmailService:
163
  發送版本更新通知郵件
164
  """
165
  if not self.is_configured():
166
- raise ValueError("Resend API not configured. Please set RESEND_API_KEY environment variable.")
167
 
168
  recipients = await self.get_active_user_emails(product_type)
169
 
@@ -171,7 +184,7 @@ class EmailService:
171
  logger.warning("No recipients found for email notification")
172
  return 0
173
 
174
- logger.info(f"Sending version announcement to {len(recipients)} recipients via Resend API")
175
 
176
  html_content = self._format_email_html(content, product_type)
177
 
@@ -179,7 +192,7 @@ class EmailService:
179
  failed_count = 0
180
 
181
  for email in recipients:
182
- result = await self._send_via_resend(email, subject, html_content)
183
  if result.get('success'):
184
  sent_count += 1
185
  else:
@@ -204,7 +217,7 @@ class EmailService:
204
  發送郵件給特定用戶
205
  """
206
  if not self.is_configured():
207
- raise ValueError("Resend API not configured. Please set RESEND_API_KEY environment variable.")
208
 
209
  if not recipients:
210
  logger.warning("No recipients provided")
@@ -218,7 +231,7 @@ class EmailService:
218
  logger.warning("No valid email addresses found")
219
  return 0
220
 
221
- logger.info(f"Sending email to {len(valid_recipients)} recipients via Resend API")
222
 
223
  html_content = self._format_email_html(content, product_type)
224
 
@@ -226,7 +239,7 @@ class EmailService:
226
  failed_count = 0
227
 
228
  for email in valid_recipients:
229
- result = await self._send_via_resend(email, subject, html_content)
230
  if result.get('success'):
231
  sent_count += 1
232
  else:
@@ -245,14 +258,14 @@ class EmailService:
245
  發送測試郵件
246
  """
247
  if not self.is_configured():
248
- raise ValueError("Resend API not configured")
249
 
250
  html_content = self._format_email_html(
251
  '這是一封測試郵件。如果您收到此郵件,表示 KSTools 郵件服務已正確設定。',
252
  'announcement'
253
  )
254
 
255
- result = await self._send_via_resend(
256
  recipient,
257
  'KSTools 郵件服務測試',
258
  html_content
@@ -270,7 +283,7 @@ class EmailService:
270
  郵件服務健康檢查
271
  """
272
  return {
273
- 'service': 'EmailService (Resend API)',
274
  'configured': self.is_configured(),
275
  'from_email': self.from_email,
276
  'from_name': self.from_name,
@@ -279,10 +292,10 @@ class EmailService:
279
 
280
  def test_connection(self) -> dict:
281
  """
282
- 測試 Resend API 連線
283
  """
284
  result = {
285
- 'service': 'Resend API',
286
  'configured': self.is_configured(),
287
  'from_email': self.from_email,
288
  'from_name': self.from_name,
@@ -291,14 +304,18 @@ class EmailService:
291
  }
292
 
293
  if not self.is_configured():
294
- result['error'] = 'Resend API not configured (missing RESEND_API_KEY)'
295
  return result
296
 
297
- # 測試 API 連線(取得 API key 資訊)
 
 
 
 
298
  try:
299
  with httpx.Client() as client:
300
  response = client.get(
301
- 'https://api.resend.com/domains',
302
  headers={
303
  'Authorization': f'Bearer {self.api_key}'
304
  },
@@ -307,11 +324,14 @@ class EmailService:
307
 
308
  if response.status_code == 200:
309
  result['connection_test'] = 'SUCCESS'
310
- domains = response.json().get('data', [])
311
- result['verified_domains'] = [d.get('name') for d in domains if d.get('status') == 'verified']
312
  elif response.status_code == 401:
313
  result['connection_test'] = 'FAILED'
314
  result['error'] = 'Invalid API key'
 
 
 
315
  else:
316
  result['connection_test'] = 'FAILED'
317
  result['error'] = f'HTTP {response.status_code}'
 
1
  """
2
  KSTools 郵件服務
3
+ 使用 SendGrid API 發送版本更新通知郵件
4
  """
5
 
6
  import os
 
18
  class EmailService:
19
  """
20
  郵件服務類別
21
+ 透過 SendGrid API 發送郵件
22
  """
23
 
24
  def __init__(self):
25
  """初始化郵件服務"""
26
+ self.api_key = os.environ.get('SENDGRID_API_KEY', '')
27
+ self.from_email = os.environ.get('SENDGRID_FROM_EMAIL', '')
28
  self.from_name = os.environ.get('FROM_NAME', 'KSTools')
29
+ self.api_url = 'https://api.sendgrid.com/v3/mail/send'
30
 
31
  if not self.api_key:
32
+ logger.warning("SENDGRID_API_KEY not configured")
33
 
34
  def is_configured(self) -> bool:
35
  """檢查郵件服務是否已設定"""
 
112
  </html>
113
  """
114
 
115
+ async def _send_via_sendgrid(self, to_email: str, subject: str, html_content: str) -> dict:
116
  """
117
+ 透過 SendGrid API 發送單封郵件
118
 
119
  Args:
120
  to_email: 收件人郵箱
 
125
  API 回應
126
  """
127
  try:
128
+ payload = {
129
+ 'personalizations': [
130
+ {
131
+ 'to': [{'email': to_email}]
132
+ }
133
+ ],
134
+ 'from': {
135
+ 'email': self.from_email,
136
+ 'name': self.from_name
137
+ },
138
+ 'subject': subject,
139
+ 'content': [
140
+ {
141
+ 'type': 'text/html',
142
+ 'value': html_content
143
+ }
144
+ ]
145
+ }
146
 
147
  async with httpx.AsyncClient() as client:
148
  response = await client.post(
 
151
  'Authorization': f'Bearer {self.api_key}',
152
  'Content-Type': 'application/json'
153
  },
154
+ json=payload,
 
 
 
 
 
155
  timeout=30
156
  )
157
 
158
+ # SendGrid 成功回傳 202 Accepted
159
+ if response.status_code == 202:
160
+ return {'success': True}
161
  else:
162
+ logger.error(f"SendGrid API error: {response.status_code} - {response.text}")
163
  return {'success': False, 'error': response.text}
164
 
165
  except Exception as e:
166
+ logger.error(f"SendGrid API call failed: {e}")
167
  return {'success': False, 'error': str(e)}
168
 
169
  async def send_version_announcement(
 
176
  發送版本更新通知郵件
177
  """
178
  if not self.is_configured():
179
+ raise ValueError("SendGrid API not configured. Please set SENDGRID_API_KEY environment variable.")
180
 
181
  recipients = await self.get_active_user_emails(product_type)
182
 
 
184
  logger.warning("No recipients found for email notification")
185
  return 0
186
 
187
+ logger.info(f"Sending version announcement to {len(recipients)} recipients via SendGrid API")
188
 
189
  html_content = self._format_email_html(content, product_type)
190
 
 
192
  failed_count = 0
193
 
194
  for email in recipients:
195
+ result = await self._send_via_sendgrid(email, subject, html_content)
196
  if result.get('success'):
197
  sent_count += 1
198
  else:
 
217
  發送郵件給特定用戶
218
  """
219
  if not self.is_configured():
220
+ raise ValueError("SendGrid API not configured. Please set SENDGRID_API_KEY environment variable.")
221
 
222
  if not recipients:
223
  logger.warning("No recipients provided")
 
231
  logger.warning("No valid email addresses found")
232
  return 0
233
 
234
+ logger.info(f"Sending email to {len(valid_recipients)} recipients via SendGrid API")
235
 
236
  html_content = self._format_email_html(content, product_type)
237
 
 
239
  failed_count = 0
240
 
241
  for email in valid_recipients:
242
+ result = await self._send_via_sendgrid(email, subject, html_content)
243
  if result.get('success'):
244
  sent_count += 1
245
  else:
 
258
  發送測試郵件
259
  """
260
  if not self.is_configured():
261
+ raise ValueError("SendGrid API not configured")
262
 
263
  html_content = self._format_email_html(
264
  '這是一封測試郵件。如果您收到此郵件,表示 KSTools 郵件服務已正確設定。',
265
  'announcement'
266
  )
267
 
268
+ result = await self._send_via_sendgrid(
269
  recipient,
270
  'KSTools 郵件服務測試',
271
  html_content
 
283
  郵件服務健康檢查
284
  """
285
  return {
286
+ 'service': 'EmailService (SendGrid API)',
287
  'configured': self.is_configured(),
288
  'from_email': self.from_email,
289
  'from_name': self.from_name,
 
292
 
293
  def test_connection(self) -> dict:
294
  """
295
+ 測試 SendGrid API 連線
296
  """
297
  result = {
298
+ 'service': 'SendGrid API',
299
  'configured': self.is_configured(),
300
  'from_email': self.from_email,
301
  'from_name': self.from_name,
 
304
  }
305
 
306
  if not self.is_configured():
307
+ result['error'] = 'SendGrid API not configured (missing SENDGRID_API_KEY)'
308
  return result
309
 
310
+ if not self.from_email:
311
+ result['error'] = 'SENDGRID_FROM_EMAIL not configured'
312
+ return result
313
+
314
+ # 測試 API 連線(使用 scopes endpoint 驗證 API key)
315
  try:
316
  with httpx.Client() as client:
317
  response = client.get(
318
+ 'https://api.sendgrid.com/v3/scopes',
319
  headers={
320
  'Authorization': f'Bearer {self.api_key}'
321
  },
 
324
 
325
  if response.status_code == 200:
326
  result['connection_test'] = 'SUCCESS'
327
+ scopes = response.json().get('scopes', [])
328
+ result['has_mail_send'] = 'mail.send' in scopes
329
  elif response.status_code == 401:
330
  result['connection_test'] = 'FAILED'
331
  result['error'] = 'Invalid API key'
332
+ elif response.status_code == 403:
333
+ result['connection_test'] = 'FAILED'
334
+ result['error'] = 'API key lacks permissions'
335
  else:
336
  result['connection_test'] = 'FAILED'
337
  result['error'] = f'HTTP {response.status_code}'