-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
702 lines (593 loc) · 29.5 KB
/
app.py
File metadata and controls
702 lines (593 loc) · 29.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
import streamlit as st
import pandas as pd
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.image import MIMEImage
from email.mime.base import MIMEBase
from email import encoders
import re
import os
from datetime import datetime
import json
from credential_manager import CredentialManager
import base64
from io import BytesIO
st.set_page_config(
page_title="Mail Sender - Bulk Email Campaign Tool",
page_icon="📧",
layout="wide"
)
if 'recipients_df' not in st.session_state:
st.session_state.recipients_df = None
if 'templates_df' not in st.session_state:
st.session_state.templates_df = None
if 'smtp_config' not in st.session_state:
# 저장된 자격 증명 로드 시도
cred_manager = CredentialManager()
saved_config = cred_manager.load_credentials()
if saved_config:
st.session_state.smtp_config = saved_config
else:
st.session_state.smtp_config = {
'server': '',
'port': 587,
'email': '',
'password': ''
}
if 'credentials_loaded' not in st.session_state:
st.session_state.credentials_loaded = False
if 'uploaded_images' not in st.session_state:
st.session_state.uploaded_images = []
if 'image_position' not in st.session_state:
st.session_state.image_position = 'bottom'
def load_file(uploaded_file):
"""CSV 또는 Excel 파일 읽기"""
if uploaded_file is not None:
try:
# 파일 포인터를 처음으로 이동
uploaded_file.seek(0)
# 파일 확장자 확인
file_extension = uploaded_file.name.lower()
if file_extension.endswith('.csv'):
# CSV 파일 읽기 - 여러 인코딩 시도
try:
df = pd.read_csv(uploaded_file, encoding='utf-8-sig')
except UnicodeDecodeError:
uploaded_file.seek(0)
try:
df = pd.read_csv(uploaded_file, encoding='cp949')
except:
uploaded_file.seek(0)
df = pd.read_csv(uploaded_file, encoding='latin1')
elif file_extension.endswith(('.xlsx', '.xls')):
df = pd.read_excel(uploaded_file, engine='openpyxl' if file_extension.endswith('.xlsx') else None)
else:
st.error(f"지원하지 않는 파일 형식입니다: {file_extension}")
return None
# 데이터 유효성 검사
if df.empty:
st.warning("파일이 비어있습니다.")
return None
st.success(f"✅ Successfully read '{uploaded_file.name}' ({len(df)} rows, {len(df.columns)} columns)")
return df
except Exception as e:
st.error(f"파일 읽기 오류: {str(e)}")
st.info("💡 팁: CSV 파일의 경우 UTF-8 인코딩으로 저장되어 있는지 확인해주세요.")
return None
return None
def substitute_template(template, row_data):
"""Replace {column_name} in template with actual data"""
result = template
pattern = r'\{([^}]+)\}'
matches = re.findall(pattern, template)
for match in matches:
if match in row_data.index:
value = str(row_data[match]) if pd.notna(row_data[match]) else ''
result = result.replace(f'{{{match}}}', value)
return result
def process_html_with_images(body, images, image_position='bottom'):
"""HTML 본문에 이미지를 인라인으로 삽입"""
# HTML 템플릿 시작
html_template = '''
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.email-container { max-width: 600px; margin: 0 auto; padding: 20px; }
.email-content { margin-bottom: 20px; }
.image-container { text-align: center; margin: 20px 0; }
.image-container img { max-width: 100%; height: auto; border-radius: 8px; margin: 10px 0; }
</style>
</head>
<body>
<div class="email-container">
'''
# Markdown을 HTML로 변환
formatted_body = body
# Bold: **text** -> <strong>text</strong>
formatted_body = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', formatted_body)
# Italic: *text* -> <em>text</em>
formatted_body = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', formatted_body)
# Underline: __text__ -> <u>text</u>
formatted_body = re.sub(r'__([^_]+)__', r'<u>\1</u>', formatted_body)
# Links: [text](url) -> <a href="url">text</a>
formatted_body = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2">\1</a>', formatted_body)
# Line break processing
formatted_body = formatted_body.replace('\n', '<br>')
# 이미지 HTML 생성
images_html = ''
if images:
images_html = '<div class="image-container">'
for i, _ in enumerate(images):
images_html += f'<img src="cid:image{i}" alt="Image {i+1}">'
images_html += '</div>'
# 위치에 따라 조합
if image_position == 'top' and images:
html_body = html_template + images_html + f'<div class="email-content">{formatted_body}</div></div></body></html>'
elif image_position == 'middle' and images:
# 본문을 반으로 나누어 중간에 이미지 삽입
lines = formatted_body.split('<br>')
mid_point = len(lines) // 2
first_half = '<br>'.join(lines[:mid_point])
second_half = '<br>'.join(lines[mid_point:])
html_body = html_template + f'<div class="email-content">{first_half}</div>' + images_html + f'<div class="email-content">{second_half}</div></div></body></html>'
else: # bottom (기본값)
html_body = html_template + f'<div class="email-content">{formatted_body}</div>' + images_html + '</div></body></html>'
return html_body
def send_email(smtp_config, recipient, subject, body, images=None, image_position='bottom'):
"""Send email (with image attachment support)"""
try:
# mixed 타입으로 변경하여 첨부파일과 인라인 이미지 모두 지원
msg = MIMEMultipart('mixed')
msg['From'] = smtp_config['email']
msg['To'] = recipient
msg['Subject'] = subject
# HTML 본문과 이미지를 위한 related 파트 생성
msg_related = MIMEMultipart('related')
# HTML 본문 처리
html_body = process_html_with_images(body, images, image_position)
html_part = MIMEText(html_body, 'html', 'utf-8')
msg_related.attach(html_part)
# 이미지 첨부
if images:
for i, img_data in enumerate(images):
# 인라인 이미지로 첨부
img = MIMEImage(img_data['bytes'])
img.add_header('Content-ID', f'<image{i}>')
img.add_header('Content-Disposition', 'inline', filename=img_data['name'])
msg_related.attach(img)
# 첨부 파일로도 추가
img_attachment = MIMEBase('application', 'octet-stream')
img_attachment.set_payload(img_data['bytes'])
encoders.encode_base64(img_attachment)
img_attachment.add_header(
'Content-Disposition',
f'attachment; filename="{img_data["name"]}"'
)
msg.attach(img_attachment)
msg.attach(msg_related)
with smtplib.SMTP(smtp_config['server'], smtp_config['port']) as server:
server.starttls()
server.login(smtp_config['email'], smtp_config['password'])
server.send_message(msg)
return True, "Success"
except Exception as e:
return False, str(e)
st.title("🎮 Mail Sender - Bulk Email Campaign Tool")
st.markdown("---")
with st.sidebar:
st.header("⚙️ SMTP Settings")
smtp_provider = st.selectbox(
"Email Provider",
["Gmail", "Naver", "Outlook", "Custom"]
)
if smtp_provider == "Gmail":
st.session_state.smtp_config['server'] = "smtp.gmail.com"
st.session_state.smtp_config['port'] = 587
elif smtp_provider == "Naver":
st.session_state.smtp_config['server'] = "smtp.naver.com"
st.session_state.smtp_config['port'] = 587
elif smtp_provider == "Outlook":
st.session_state.smtp_config['server'] = "smtp-mail.outlook.com"
st.session_state.smtp_config['port'] = 587
st.session_state.smtp_config['server'] = st.text_input(
"SMTP Server",
value=st.session_state.smtp_config['server']
)
st.session_state.smtp_config['port'] = st.number_input(
"Port",
value=st.session_state.smtp_config['port']
)
st.session_state.smtp_config['email'] = st.text_input(
"Sender Email",
value=st.session_state.smtp_config['email']
)
st.session_state.smtp_config['password'] = st.text_input(
"Password",
value=st.session_state.smtp_config['password'],
type="password"
)
if smtp_provider == "Gmail":
st.info("For Gmail, use an app password.\n[Generate app password](https://myaccount.google.com/apppasswords)")
# 자격 증명 저장/로드 버튼
st.markdown("---")
col1, col2 = st.columns(2)
with col1:
if st.button("💾 Save Settings", use_container_width=True):
cred_manager = CredentialManager()
if cred_manager.save_credentials(st.session_state.smtp_config):
st.success("Settings saved securely!")
else:
st.error("Failed to save settings.")
with col2:
if st.button("🗑️ Delete Saved Settings", use_container_width=True):
cred_manager = CredentialManager()
if cred_manager.delete_credentials():
st.success("Saved settings deleted.")
st.session_state.smtp_config = {
'server': '',
'port': 587,
'email': '',
'password': ''
}
st.rerun()
else:
st.error("Failed to delete settings.")
if st.session_state.smtp_config['email'] and st.session_state.smtp_config['password']:
st.success("✅ SMTP configuration complete")
tab1, tab2, tab3, tab4, tab5 = st.tabs(["📊 Recipients", "📝 Templates", "🖼️ Images", "👁️ Preview", "📤 Send"])
with tab1:
st.header("Recipient Data Management")
col1, col2 = st.columns([2, 1])
with col1:
uploaded_recipients = st.file_uploader(
"Upload Recipients File (CSV/Excel)",
type=['csv', 'xlsx', 'xls'],
key="recipients_upload",
help="Select CSV or Excel file. File must contain 'email' column."
)
if uploaded_recipients:
with st.spinner(f"Reading '{uploaded_recipients.name}' file..."):
df = load_file(uploaded_recipients)
if df is not None:
st.session_state.recipients_df = df
# 컬럼 정보 표시
st.info(f"📊 Available columns: {', '.join(df.columns.tolist())}")
with col2:
if st.button("Generate Sample Data", type="secondary"):
sample_data = {
'email': ['user1@example.com', 'user2@example.com', 'user3@example.com'],
'name': ['John Doe', 'Jane Smith', 'Mike Wilson'],
'game_title': ['Super RPG', 'Puzzle Master', 'Action Hero'],
'discount': ['30%', '50%', '20%']
}
st.session_state.recipients_df = pd.DataFrame(sample_data)
st.success("Sample data generated!")
if st.session_state.recipients_df is not None:
st.subheader("Edit Data")
edited_df = st.data_editor(
st.session_state.recipients_df,
num_rows="dynamic",
use_container_width=True,
key="recipients_editor"
)
if edited_df is not None and not edited_df.equals(st.session_state.recipients_df):
st.session_state.recipients_df = edited_df
col1, col2 = st.columns(2)
with col1:
st.metric("Total Recipients", len(st.session_state.recipients_df))
with col2:
if 'email' in st.session_state.recipients_df.columns:
valid_emails = st.session_state.recipients_df['email'].notna().sum()
st.metric("Valid Emails", valid_emails)
csv = st.session_state.recipients_df.to_csv(index=False).encode('utf-8-sig')
st.download_button(
label="📥 Save as CSV",
data=csv,
file_name=f"recipients_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
mime="text/csv"
)
with tab2:
st.header("Email Template Management")
col1, col2 = st.columns([2, 1])
with col1:
uploaded_templates = st.file_uploader(
"Upload Template File (CSV/Excel)",
type=['csv', 'xlsx', 'xls'],
key="templates_upload",
help="Select CSV or Excel file. File must contain 'subject' and 'body' columns."
)
if uploaded_templates:
with st.spinner(f"Reading '{uploaded_templates.name}' file..."):
df = load_file(uploaded_templates)
if df is not None:
st.session_state.templates_df = df
# 컬럼 정보 표시
st.info(f"📊 Available columns: {', '.join(df.columns.tolist())}")
# 필수 컬럼 검증
required_cols = ['subject', 'body']
missing_cols = [col for col in required_cols if col not in df.columns]
if missing_cols:
st.warning(f"⚠️ Missing required columns: {', '.join(missing_cols)}")
with col2:
if st.button("Generate Sample Templates", type="secondary"):
sample_templates = {
'template_name': ['Basic Promotion', 'Discount Event', 'New Release'],
'subject': [
'🎮 {name}, try {game_title} today!',
'💸 Special {discount} discount for {name}!',
'🚀 New release: {game_title}! {name}, be the first to know'
],
'body': [
'Hello {name}!\n\nIntroducing {game_title}.\nExperience exciting gameplay!\n\nStart now: [Game Link]',
'Hello {name}!\n\nSpecial {discount} discount event!\nGet {game_title} at a special price.\n\nLimited time offer.',
'Dear {name},\n\n{game_title} has finally launched!\nDon\'t miss the early bird benefits.\n\nThank you.'
]
}
st.session_state.templates_df = pd.DataFrame(sample_templates)
st.success("Sample templates generated!")
if st.session_state.templates_df is not None:
st.subheader("Edit Templates")
edited_templates = st.data_editor(
st.session_state.templates_df,
num_rows="dynamic",
use_container_width=True,
key="templates_editor"
)
if edited_templates is not None and not edited_templates.equals(st.session_state.templates_df):
st.session_state.templates_df = edited_templates
st.info("💡 Tip: Use {column_name} format for variables to be automatically substituted with recipient data.")
csv = st.session_state.templates_df.to_csv(index=False).encode('utf-8-sig')
st.download_button(
label="📥 Save Templates as CSV",
data=csv,
file_name=f"templates_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
mime="text/csv"
)
with tab3:
st.header("Image Management")
st.subheader("📸 Upload Images")
uploaded_images = st.file_uploader(
"Select image files (multiple selection allowed)",
type=['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'],
accept_multiple_files=True,
key="image_upload"
)
if uploaded_images:
st.session_state.uploaded_images = []
cols = st.columns(min(len(uploaded_images), 4))
for idx, img_file in enumerate(uploaded_images):
try:
# 이미지 데이터 저장
img_file.seek(0) # 파일 포인터를 처음으로 이동
img_bytes = img_file.read()
if len(img_bytes) == 0:
st.error(f"⚠️ '{img_file.name}' 파일을 읽을 수 없습니다. 파일이 비어있거나 손상되었습니다.")
continue
st.session_state.uploaded_images.append({
'name': img_file.name,
'bytes': img_bytes,
'size': len(img_bytes)
})
except Exception as e:
st.error(f"⚠️ '{img_file.name}' 파일 처리 중 오류 발생: {str(e)}")
continue
# 업로드 성공한 이미지들만 미리보기 표시
if st.session_state.uploaded_images:
st.success(f"Total {len(st.session_state.uploaded_images)} images uploaded.")
# 미리보기 표시
preview_cols = st.columns(min(len(st.session_state.uploaded_images), 4))
for idx, img_data in enumerate(st.session_state.uploaded_images):
with preview_cols[idx % len(preview_cols)]:
st.image(img_data['bytes'], caption=img_data['name'], use_column_width=True)
st.caption(f"Size: {img_data['size'] / 1024:.1f} KB")
else:
st.warning("No images available to upload. Please select files again.")
# 이미지 순서 관리
if len(st.session_state.uploaded_images) > 1:
st.subheader("🔄 Adjust Image Order")
st.info("Images will be included in the email in the order they were uploaded.")
else:
if st.session_state.uploaded_images:
st.info(f"Currently {len(st.session_state.uploaded_images)} images are stored.")
cols = st.columns(min(len(st.session_state.uploaded_images), 4))
for idx, img_data in enumerate(st.session_state.uploaded_images):
with cols[idx % len(cols)]:
st.image(img_data['bytes'], caption=img_data['name'], use_column_width=True)
st.caption(f"Size: {img_data['size'] / 1024:.1f} KB")
if st.button("🗑️ Remove All Images", type="secondary"):
st.session_state.uploaded_images = []
st.rerun()
else:
st.info("Please upload images to attach to the email.")
# 이미지 위치 설정
if st.session_state.uploaded_images:
st.subheader("📍 Image Position Settings")
st.session_state.image_position = st.radio(
"Image position in email body",
options=['top', 'middle', 'bottom'],
format_func=lambda x: {'top': '🔝 Top', 'middle': '🔀 Middle', 'bottom': '⬇️ Bottom'}[x],
horizontal=True,
index=['top', 'middle', 'bottom'].index(st.session_state.image_position)
)
# 이미지 사용 안내
with st.expander("ℹ️ Image Usage Guide"):
st.markdown("""
- **인라인 이미지**: 선택한 위치에 이미지가 표시됩니다.
- **첨부 파일**: 동시에 첨부 파일로도 포함됩니다.
- **최대 크기**: 일반적으로 이미지당 5MB 이하를 권장합니다.
- **형식**: PNG, JPG, GIF 등 주요 이미지 형식을 지원합니다.
- **순서**: 업로드한 순서대로 이메일에 표시됩니다.
- **위치 옵션**:
- 상단: 본문 시작 전에 이미지 표시
- 중간: 본문 중간에 이미지 삽입
- 하단: 본문 끝에 이미지 표시 (기본값)
""")
with tab4:
st.header("메일 미리보기")
if st.session_state.recipients_df is not None and st.session_state.templates_df is not None:
col1, col2 = st.columns(2)
with col1:
if 'email' in st.session_state.recipients_df.columns:
selected_recipient = st.selectbox(
"Select Recipient",
st.session_state.recipients_df.index,
format_func=lambda x: f"{st.session_state.recipients_df.loc[x, 'email']} ({st.session_state.recipients_df.loc[x, 'name'] if 'name' in st.session_state.recipients_df.columns else 'Unknown'})"
)
else:
st.warning("No 'email' column in recipient data.")
selected_recipient = None
with col2:
if 'template_name' in st.session_state.templates_df.columns:
selected_template = st.selectbox(
"Select Template",
st.session_state.templates_df.index,
format_func=lambda x: st.session_state.templates_df.loc[x, 'template_name']
)
else:
selected_template = st.selectbox(
"Select Template",
st.session_state.templates_df.index
)
if selected_recipient is not None and selected_template is not None:
recipient_data = st.session_state.recipients_df.loc[selected_recipient]
template_data = st.session_state.templates_df.loc[selected_template]
if 'subject' in template_data and 'body' in template_data:
subject = substitute_template(str(template_data['subject']), recipient_data)
body = substitute_template(str(template_data['body']), recipient_data)
st.subheader("📧 미리보기")
with st.container():
st.markdown(f"**수신:** {recipient_data['email']}")
st.markdown(f"**제목:** {subject}")
# 이미지 미리보기 (위치에 따라 표시)
if st.session_state.uploaded_images:
st.markdown("---")
position_text = {'top': '상단', 'middle': '중간', 'bottom': '하단'}[st.session_state.image_position]
st.markdown(f"**📍 이미지 위치: {position_text}**")
# 상단 이미지
if st.session_state.image_position == 'top':
img_cols = st.columns(min(len(st.session_state.uploaded_images), 3))
for idx, img_data in enumerate(st.session_state.uploaded_images):
with img_cols[idx % len(img_cols)]:
st.image(img_data['bytes'], caption=img_data['name'], use_column_width=True)
st.markdown("---")
st.markdown("**본문:**")
st.text_area("", body, height=200, disabled=True, key="preview_body_top")
# 중간 이미지
elif st.session_state.image_position == 'middle':
lines = body.split('\n')
mid_point = len(lines) // 2
first_half = '\n'.join(lines[:mid_point])
second_half = '\n'.join(lines[mid_point:])
st.markdown("**본문 (상단):**")
st.text_area("", first_half, height=100, disabled=True, key="preview_body_first")
img_cols = st.columns(min(len(st.session_state.uploaded_images), 3))
for idx, img_data in enumerate(st.session_state.uploaded_images):
with img_cols[idx % len(img_cols)]:
st.image(img_data['bytes'], caption=img_data['name'], use_column_width=True)
st.markdown("**본문 (하단):**")
st.text_area("", second_half, height=100, disabled=True, key="preview_body_second")
# 하단 이미지 (기본)
else:
st.markdown("**본문:**")
st.text_area("", body, height=200, disabled=True, key="preview_body_bottom")
st.markdown("---")
img_cols = st.columns(min(len(st.session_state.uploaded_images), 3))
for idx, img_data in enumerate(st.session_state.uploaded_images):
with img_cols[idx % len(img_cols)]:
st.image(img_data['bytes'], caption=img_data['name'], use_column_width=True)
else:
# 이미지가 없을 때
st.markdown("**본문:**")
st.text_area("", body, height=200, disabled=True, key="preview_body_no_image")
else:
st.warning("템플릿에 'subject'와 'body' 컬럼이 필요합니다.")
else:
st.info("수신자 데이터와 템플릿을 먼저 업로드해주세요.")
with tab5:
st.header("메일 발송")
if st.session_state.recipients_df is not None and st.session_state.templates_df is not None:
if 'email' not in st.session_state.recipients_df.columns:
st.error("Recipient data must have 'email' column.")
elif 'subject' not in st.session_state.templates_df.columns or 'body' not in st.session_state.templates_df.columns:
st.error("템플릿 데이터에 'subject'와 'body' 컬럼이 필요합니다.")
else:
col1, col2 = st.columns(2)
with col1:
template_idx = st.selectbox(
"Template to use",
st.session_state.templates_df.index,
format_func=lambda x: st.session_state.templates_df.loc[x, 'template_name'] if 'template_name' in st.session_state.templates_df.columns else f"템플릿 {x}",
key="send_template"
)
with col2:
test_mode = st.checkbox("Test mode (send to first recipient only)", value=True)
template = st.session_state.templates_df.loc[template_idx]
# 이미지 첨부 옵션
st.subheader("🖼️ 이미지 첨부 설정")
if st.session_state.uploaded_images:
use_images = st.checkbox(
f"Include {len(st.session_state.uploaded_images)} images in email",
value=True
)
if use_images:
total_size = sum(img['size'] for img in st.session_state.uploaded_images) / 1024 / 1024
st.info(f"총 이미지 크기: {total_size:.2f} MB")
if total_size > 10:
st.warning("⚠️ Large image sizes may be rejected by some mail servers.")
else:
use_images = False
st.info("첨부할 이미지가 없습니다.")
if st.button("📤 Send Emails", type="primary"):
if not st.session_state.smtp_config['email'] or not st.session_state.smtp_config['password']:
st.error("SMTP 설정을 먼저 완료해주세요.")
else:
recipients = st.session_state.recipients_df.head(1) if test_mode else st.session_state.recipients_df
progress_bar = st.progress(0)
status_text = st.empty()
results = []
total = len(recipients)
for idx, (index, row) in enumerate(recipients.iterrows()):
progress = (idx + 1) / total
progress_bar.progress(progress)
status_text.text(f"Sending... {idx + 1}/{total}")
email = row['email']
subject = substitute_template(str(template['subject']), row)
body = substitute_template(str(template['body']), row)
# 이미지 포함 여부에 따라 발송
if use_images and st.session_state.uploaded_images:
success, message = send_email(
st.session_state.smtp_config,
email,
subject,
body,
st.session_state.uploaded_images,
st.session_state.image_position
)
else:
success, message = send_email(
st.session_state.smtp_config,
email,
subject,
body
)
results.append({
'email': email,
'status': '✅ Success' if success else '❌ Failed',
'message': message
})
progress_bar.empty()
status_text.empty()
st.success(f"Sending complete! Total: {total}")
results_df = pd.DataFrame(results)
st.dataframe(results_df, use_container_width=True)
csv = results_df.to_csv(index=False).encode('utf-8-sig')
st.download_button(
label="📥 Download Send Results",
data=csv,
file_name=f"send_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
mime="text/csv"
)
else:
st.info("수신자 데이터와 템플릿을 먼저 업로드해주세요.")