1
1
/* eslint-disable no-console */
2
2
import { getModeName , type ENV , type HonoType } from "@api/lib/consts" ;
3
3
import { authGuard , configureCors } from "@api/lib/middlewares/contact" ;
4
- import { type EmailAddress , getPersonalizationInfo } from "@api/lib/sender" ;
4
+ import { sendEmail } from "@api/lib/sender" ;
5
5
import { INFO } from "@client/lib/config" ;
6
6
import { formatDate , getEntries } from "@client/lib/consts" ;
7
7
import {
8
8
type ContactFormData ,
9
9
formSchema ,
10
10
zContactFormData ,
11
11
} from "@client/lib/services/contact" ;
12
- import { sendEmail } from "@cloudflare/pages-plugin-mailchannels/api" ;
13
12
import { zValidator } from "@hono/zod-validator" ;
14
13
import { Hono } from "hono" ;
15
14
import { cors } from "hono/cors" ;
16
15
import { HTTPException } from "hono/http-exception" ;
16
+ import { err , ok , ResultAsync } from "neverthrow" ;
17
17
18
- async function sendAdminMail (
18
+ function formatContactData ( data : ContactFormData ) {
19
+ return getEntries ( formSchema )
20
+ . filter ( ( [ k ] ) => data [ k ] !== "" )
21
+ . map ( ( [ key , { description } ] ) =>
22
+ `
23
+ ${ description } :
24
+ ${ data [ key ] }
25
+ ` . trim ( ) ,
26
+ )
27
+ . join ( "\n" ) ;
28
+ }
29
+
30
+ function sendAdminMail (
19
31
data : ContactFormData ,
20
32
env : ENV ,
21
- acceptDate : Date ,
33
+ acceptedDate : Date ,
22
34
) : ReturnType < typeof sendEmail > {
23
- const to : EmailAddress = {
24
- email : INFO . addr . email . contact ,
25
- name : INFO . name . full ,
26
- } ;
27
- const from : EmailAddress = {
28
- email : INFO . addr . email . noreply ,
29
- name : INFO . name . full ,
30
- } ;
31
35
const subject = `【お問い合わせ】${ data . name } 様からのお問い合わせ - ${ INFO . id } ` ;
32
36
33
- const _data = getEntries ( formSchema )
34
- . map ( ( [ key , { description } ] ) =>
35
- `
36
- ${ description } :
37
- ${ data [ key ] }
38
- ` . trim ( ) ,
39
- )
40
- . join ( "\n" ) ;
41
37
const content = `
42
38
${ getModeName ( env . MODE ) }
43
39
ポートフォリオのお問い合わせフォームから以下の内容が送信されました。
44
40
お問い合わせ内容を確認し、返信をお願いします。
45
41
46
42
--- お問い合わせ内容 ---
47
- ${ _data }
43
+ ${ formatContactData ( data ) }
48
44
---
49
- 受付日時: ${ formatDate ( acceptDate , "YYYY-MM-DD HH:mm:ss" ) }
45
+ 受付日時: ${ formatDate ( acceptedDate , "YYYY-MM-DD HH:mm:ss" ) }
50
46
` . trim ( ) ;
51
47
52
- return await sendEmail ( {
53
- personalizations : [
54
- getPersonalizationInfo ( {
55
- env,
56
- info : { to : [ to ] , from } ,
57
- } ) ,
58
- ] ,
59
- content : [
60
- {
61
- type : "text/plain" ,
62
- value : content ,
63
- } ,
64
- ] ,
48
+ return sendEmail ( env , {
49
+ to : INFO . addr . email . admin ,
65
50
subject,
66
- from ,
51
+ text : content ,
67
52
} ) ;
68
53
}
69
54
70
- async function sendThanksMail (
55
+ function sendThanksMail (
71
56
data : ContactFormData ,
72
57
env : ENV ,
73
58
acceptDate : Date ,
74
59
) : ReturnType < typeof sendEmail > {
75
- const to : EmailAddress = {
76
- email : data . email ,
77
- name : `${ data . name } 様` ,
78
- } ;
79
- const from : EmailAddress = {
80
- email : INFO . addr . email . noreply ,
81
- name : INFO . name . full ,
82
- } ;
83
60
const subject = `【自動返信】お問い合わせありがとうございます - ${ INFO . name . full } ` ;
84
61
85
- const _data = getEntries ( formSchema )
86
- . map ( ( [ key , { description } ] ) =>
87
- `
88
- ${ description } :
89
- ${ data [ key ] }
90
- ` . trim ( ) ,
91
- )
92
- . join ( "\n" ) ;
62
+ const deadlineDate = new Date ( acceptDate ) ;
63
+ deadlineDate . setDate ( acceptDate . getDate ( ) + 3 ) ;
64
+ const deadlineDateStr = formatDate ( deadlineDate , "YYYY年M月d日" ) ;
93
65
94
66
const content = `
95
67
${ getModeName ( env . MODE ) }
96
- テストテストテストテストテストテスト
97
- テストテストテストテストテストテスト
68
+ この度は、お問い合わせいただき誠にありがとうございます。
69
+ 以下の内容でお問い合わせを受け付けいたしました。
98
70
99
71
--- お問い合わせ内容 ---
100
- ${ _data }
72
+ ${ formatContactData ( data ) }
101
73
---
102
74
103
- テストテストテストテストテストテスト
75
+ もしも${ deadlineDateStr } までに返信がない場合、お手数ですが${ INFO . addr . email . contact } まで再度ご連絡いただきますようお願いいたします。
76
+
77
+ ※このメールは自動返信により送信しています。ご返信をいただいても対応できかねますことをご了承ください。
104
78
` . trim ( ) ;
105
79
106
- return await sendEmail ( {
107
- personalizations : [
108
- getPersonalizationInfo ( {
109
- env,
110
- info : { to : [ to ] , from } ,
111
- } ) ,
112
- ] ,
113
- content : [
114
- {
115
- type : "text/plain" ,
116
- value : content ,
117
- } ,
118
- ] ,
80
+ return sendEmail ( env , {
81
+ to : data . email ,
119
82
subject,
120
- from ,
83
+ text : content ,
121
84
} ) ;
122
85
}
123
86
124
- async function sendDiscordWebhook (
87
+ function sendDiscordWebhook (
125
88
data : ContactFormData ,
126
89
env : ENV ,
127
90
acceptDate : Date ,
128
- ) : Promise < Response > {
91
+ ) : ResultAsync <
92
+ undefined ,
93
+ { code : "NETWORK_ERROR" | "API_ERROR" ; details : string }
94
+ > {
129
95
const unixTime = Math . floor ( acceptDate . getTime ( ) / 1000 ) ;
130
96
const content = `
131
97
<:9u3rcusdark:1204434658792837160> <@${ env . API_DISCORD_WEBHOOK_MENTION_ID } >
@@ -156,12 +122,25 @@ async function sendDiscordWebhook(
156
122
attachments : [ ] ,
157
123
} ;
158
124
159
- return await fetch ( env . API_DISCORD_WEBHOOK_URL_CONTACT , {
160
- method : "POST" ,
161
- headers : {
162
- "Content-Type" : "application/json" ,
163
- } ,
164
- body : JSON . stringify ( body ) ,
125
+ return ResultAsync . fromPromise (
126
+ fetch ( env . API_DISCORD_WEBHOOK_URL_CONTACT , {
127
+ method : "POST" ,
128
+ headers : {
129
+ "Content-Type" : "application/json" ,
130
+ } ,
131
+ body : JSON . stringify ( body ) ,
132
+ } ) ,
133
+ ( e ) => ( { code : "NETWORK_ERROR" , details : String ( e ) } ) as const ,
134
+ ) . andThen ( ( res ) => {
135
+ if ( ! res . ok ) {
136
+ console . error ( "Failed to send discord webhook" , res ) ;
137
+ return err ( {
138
+ code : "API_ERROR" ,
139
+ details : `API returned status: ${ res . statusText } ` ,
140
+ } as const ) ;
141
+ }
142
+
143
+ return ok ( undefined ) ;
165
144
} ) ;
166
145
}
167
146
@@ -170,33 +149,34 @@ export const contact = new Hono<HonoType>()
170
149
. use ( "*" , authGuard , configureCors )
171
150
. post ( "/" , zValidator ( "json" , zContactFormData ) , async ( ctx ) => {
172
151
const data = ctx . req . valid ( "json" ) ;
173
- const acceptDate = new Date ( ) ;
152
+ const acceptedDate = new Date ( ) ;
174
153
175
- if ( ctx . env . MODE !== "local" ) {
176
- const adminMailResult = await sendAdminMail ( data , ctx . env , acceptDate ) ;
177
- if ( ! adminMailResult . success ) {
178
- console . warn ( "Failed to send admin mail" , adminMailResult . errors ) ;
154
+ await sendAdminMail ( data , ctx . env , acceptedDate ) . match (
155
+ ( ) => { } ,
156
+ ( ) => {
179
157
throw new HTTPException ( 500 , {
180
158
message : "Failed to send admin mail" ,
181
159
} ) ;
182
- }
160
+ } ,
161
+ ) ;
183
162
184
- const autoReplyResult = await sendThanksMail ( data , ctx . env , acceptDate ) ;
185
- if ( ! autoReplyResult . success ) {
186
- console . warn ( "Failed to send auto thanks mail" , autoReplyResult . errors ) ;
163
+ await sendThanksMail ( data , ctx . env , acceptedDate ) . match (
164
+ ( ) => { } ,
165
+ ( ) => {
187
166
throw new HTTPException ( 500 , {
188
167
message : "Failed to send thanks mail" ,
189
168
} ) ;
190
- }
191
- }
169
+ } ,
170
+ ) ;
192
171
193
- const res = await sendDiscordWebhook ( data , ctx . env , acceptDate ) ;
194
- if ( ! res . ok ) {
195
- console . warn ( "Failed to send discord webhook" , res ) ;
196
- throw new HTTPException ( 500 , {
197
- message : "Failed to send discord webhook" ,
198
- } ) ;
199
- }
172
+ await sendDiscordWebhook ( data , ctx . env , acceptedDate ) . match (
173
+ ( ) => { } ,
174
+ ( ) => {
175
+ throw new HTTPException ( 500 , {
176
+ message : "Failed to send discord webhook" ,
177
+ } ) ;
178
+ } ,
179
+ ) ;
200
180
201
- return ctx . json ( { acceptDate } , 201 ) ;
181
+ return ctx . json ( { acceptedDate } , 201 ) ;
202
182
} ) ;
0 commit comments