@@ -69,21 +69,80 @@ <h2 class="font-display text-lg font-semibold">Slack webhook</h2>
6969 </ section >
7070
7171 <!-- ====================================================== -->
72- <!-- SMTP / EMAIL — placeholder for the next iteration -->
72+ <!-- SMTP / EMAIL -->
7373 <!-- ====================================================== -->
74- < section class ="glass rounded-xl p-6 opacity-70 ">
74+ < section class ="glass rounded-xl p-6 ">
7575 < div class ="flex items-center gap-2 ">
76- < span class ="grid h-9 w-9 place-items-center rounded-md border border-white/10 bg-white/[0.04] text-slate -400 ">
76+ < span class ="grid h-9 w-9 place-items-center rounded-md border border-white/10 bg-white/[0.04] text-accent -400 ">
7777 < svg viewBox ="0 0 24 24 " class ="h-4 w-4 " fill ="none " stroke ="currentColor " stroke-width ="2 " stroke-linecap ="round " stroke-linejoin ="round ">
7878 < path d ="M4 4h16v16H4z "/> < path d ="M4 4l8 8 8-8 "/>
7979 </ svg >
8080 </ span >
8181 < h2 class ="font-display text-lg font-semibold "> Email (SMTP)</ h2 >
82- < span class ="rounded-md border border-white/10 bg-white/[0.03] px-2 py-0.5 text-[10px] font-medium uppercase tracking-widest text-slate-500 "> coming soon</ span >
82+ < template x-if ="hasSmtp ">
83+ < span class ="rounded-md border border-accent-500/30 bg-accent-500/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-widest text-accent-400 "> configured</ span >
84+ </ template >
8385 </ div >
8486 < p class ="mt-2 text-sm leading-relaxed text-slate-400 ">
85- SMTP host / port / user / pass / from-address. Same encryption-at-rest model as Slack .
87+ Same encryption-at-rest model as Slack. Connection uses STARTTLS by default — works with Gmail (port 587), SES, Mailgun, Postmark, etc .
8688 </ p >
89+
90+ < div class ="mt-5 grid gap-3 sm:grid-cols-2 ">
91+ < label class ="text-xs ">
92+ < span class ="block font-medium text-slate-300 "> SMTP host</ span >
93+ < input x-model ="smtp.host " type ="text " placeholder ="smtp.gmail.com "
94+ class ="focus-ring mt-1 w-full rounded-md border border-white/10 bg-ink-950/60 px-3 py-2 text-sm placeholder-slate-600 ">
95+ </ label >
96+ < label class ="text-xs ">
97+ < span class ="block font-medium text-slate-300 "> Port</ span >
98+ < input x-model.number ="smtp.port " type ="number " min ="1 " max ="65535 " placeholder ="587 "
99+ class ="focus-ring mt-1 w-full rounded-md border border-white/10 bg-ink-950/60 px-3 py-2 text-sm placeholder-slate-600 ">
100+ </ label >
101+ < label class ="text-xs ">
102+ < span class ="block font-medium text-slate-300 "> Username</ span >
103+ < input x-model ="smtp.user " type ="text " placeholder ="ops@example.com " autocomplete ="off "
104+ class ="focus-ring mt-1 w-full rounded-md border border-white/10 bg-ink-950/60 px-3 py-2 text-sm placeholder-slate-600 ">
105+ </ label >
106+ < label class ="text-xs ">
107+ < span class ="block font-medium text-slate-300 "> Password / app token</ span >
108+ < input x-model ="smtp.password " type ="password " placeholder ="••••••••• " autocomplete ="new-password "
109+ class ="focus-ring mt-1 w-full rounded-md border border-white/10 bg-ink-950/60 px-3 py-2 text-sm placeholder-slate-600 ">
110+ </ label >
111+ < label class ="text-xs sm:col-span-2 ">
112+ < span class ="block font-medium text-slate-300 "> From address</ span >
113+ < input x-model ="smtp.from_addr " type ="email " placeholder ="alerts@example.com "
114+ class ="focus-ring mt-1 w-full rounded-md border border-white/10 bg-ink-950/60 px-3 py-2 text-sm placeholder-slate-600 ">
115+ </ label >
116+ </ div >
117+
118+ < div class ="mt-4 flex flex-wrap items-center gap-3 ">
119+ < label class ="flex items-center gap-2 text-xs text-slate-400 ">
120+ < input x-model ="smtp.use_tls " type ="checkbox " class ="rounded border-white/10 bg-ink-950/60 text-accent-500 focus:ring-accent-500/40 ">
121+ Use STARTTLS
122+ </ label >
123+ < label class ="ml-auto text-xs ">
124+ < span class ="block font-medium text-slate-300 "> Send test to</ span >
125+ < input x-model ="smtpTestTo " type ="email " placeholder ="defaults to From address "
126+ class ="focus-ring mt-1 w-64 rounded-md border border-white/10 bg-ink-950/60 px-3 py-1.5 text-xs placeholder-slate-600 ">
127+ </ label >
128+ </ div >
129+
130+ < div class ="mt-4 flex flex-wrap gap-3 ">
131+ < button type ="button " @click ="saveSmtp() " :disabled ="saving || !smtpComplete "
132+ class ="rounded-md bg-accent-500 px-4 py-2 text-sm font-medium text-ink-950 shadow-sm transition hover:bg-accent-400 disabled:opacity-50 ">
133+ < span x-show ="!saving "> Save</ span >
134+ < span x-show ="saving "> Saving…</ span >
135+ </ button >
136+ < button type ="button " @click ="testSmtp() " :disabled ="!hasSmtp || testing "
137+ class ="rounded-md border border-white/10 bg-white/[0.03] px-4 py-2 text-sm text-slate-200 transition hover:border-accent-500/40 hover:text-accent-400 disabled:opacity-40 ">
138+ < span x-show ="!testing "> Send test email</ span >
139+ < span x-show ="testing "> Sending…</ span >
140+ </ button >
141+ < template x-if ="hasSmtp ">
142+ < button type ="button " @click ="remove('smtp_config') "
143+ class ="ml-auto text-xs text-slate-500 hover:text-rose2-400 "> Remove SMTP config</ button >
144+ </ template >
145+ </ div >
87146 </ section >
88147</ div >
89148
@@ -92,11 +151,19 @@ <h2 class="font-display text-lg font-semibold">Email (SMTP)</h2>
92151 return {
93152 configured : [ ] , // [{kind, updated_at}]
94153 slackValue : '' ,
154+ smtp : { host : '' , port : 587 , user : '' , password : '' , from_addr : '' , use_tls : true } ,
155+ smtpTestTo : '' ,
95156 saving : false ,
96157 testing : false ,
97158 message : '' ,
98159 messageKind : 'success' ,
99160 get hasSlack ( ) { return this . configured . some ( c => c . kind === 'slack_webhook' ) ; } ,
161+ get hasSmtp ( ) { return this . configured . some ( c => c . kind === 'smtp_config' ) ; } ,
162+ get smtpComplete ( ) {
163+ const s = this . smtp ;
164+ return s . host && s . port && s . user && s . password &&
165+ s . from_addr && s . from_addr . includes ( '@' ) ;
166+ } ,
100167 flash ( text , kind ) { this . message = text ; this . messageKind = kind || 'success' ;
101168 setTimeout ( ( ) => { this . message = '' ; } , 6000 ) ; } ,
102169 async load ( ) {
@@ -146,6 +213,38 @@ <h2 class="font-display text-lg font-semibold">Email (SMTP)</h2>
146213 } catch ( e ) { this . flash ( e . message , 'error' ) ; }
147214 finally { this . testing = false ; }
148215 } ,
216+ async saveSmtp ( ) {
217+ if ( ! this . smtpComplete ) return ;
218+ this . saving = true ;
219+ try {
220+ const r = await fetch ( '/api/connectors/save' , {
221+ method : 'POST' , headers : { 'Content-Type' : 'application/json' } ,
222+ body : JSON . stringify ( { kind : 'smtp_config' , value : this . smtp } ) ,
223+ } ) ;
224+ const body = await r . json ( ) ;
225+ if ( ! r . ok ) throw new Error ( body . detail || 'save failed' ) ;
226+ // Don't keep the password in memory after save.
227+ this . smtp . password = '' ;
228+ await this . load ( ) ;
229+ this . flash ( 'SMTP saved.' , 'success' ) ;
230+ } catch ( e ) { this . flash ( e . message , 'error' ) ; }
231+ finally { this . saving = false ; }
232+ } ,
233+ async testSmtp ( ) {
234+ this . testing = true ;
235+ try {
236+ const body_ = { kind : 'smtp_config' } ;
237+ if ( this . smtpTestTo ) body_ . to = this . smtpTestTo ;
238+ const r = await fetch ( '/api/connectors/test' , {
239+ method : 'POST' , headers : { 'Content-Type' : 'application/json' } ,
240+ body : JSON . stringify ( body_ ) ,
241+ } ) ;
242+ const body = await r . json ( ) ;
243+ if ( ! r . ok ) throw new Error ( body . detail || 'test failed' ) ;
244+ this . flash ( '✓ Test email sent to ' + body . to , 'success' ) ;
245+ } catch ( e ) { this . flash ( e . message , 'error' ) ; }
246+ finally { this . testing = false ; }
247+ } ,
149248 } ;
150249 }
151250</ script >
0 commit comments