ブライダルフェアの予約確認に「カレンダーに追加」を付ける実装。.ics(iCalendar)の書き方、日本時間の扱い、日時の変更と取り消しの送り直し、確認メールへの添付と予約完了ページからの配り方
フェアや
この
この
この
「カレンダーに追加」は、来館の予定をお客様の手元に残す仕組み
予約確認メールは、
.ics の
一方で、
| 置く場所 | 載せる内容 | 役割 |
|---|---|---|
| 予約確認メールの |
日時・場所・所要時間・変更と |
正本。 |
| 添付の |
日時・場所・所要時間・式場からの |
お客様の |
| 予約完了ページ・予約確認ページ | 日時・場所と、 |
メールが |
予約確認メール
.ics の最小の中身と、RFC 5545 で決まっていること
フェア予約1件ぶんの
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//example//fair-booking//JA
METHOD:PUBLISH
BEGIN:VEVENT
UID:6f1c2b9e-3d4a-4f5b-8c7d-0e1f2a3b4c5d
SEQUENCE:0
DTSTAMP:20261006T121500Z
DTSTART:20261114T010000Z
DTEND:20261114T040000Z
SUMMARY:ブライダルフェア(試食つき)のご来館
LOCATION:サンプル会場(〒000-0000 サンプル県サンプル市1-2
-3)
DESCRIPTION:所要時間は約3時間です。\n駐車場:あり(30台
)\n日時の変更・取り消しは、予約確認メールのリンク
か、お電話でお知らせください。
ORGANIZER;CN="サンプル会場 ブライダルサロン":mailto:fair@exam
ple.jp
STATUS:CONFIRMED
URL:https://example.jp/fair/booking/
END:VEVENT
END:VCALENDAR会場名・住所・アドレスは
| 行 | 決まっている |
この |
|---|---|---|
| VERSION・PRODID | VCALENDAR に |
PRODID は |
| METHOD | 書くと、 |
PUBLISH |
| UID・DTSTAMP | VEVENT に |
予約ごとの |
| DTSTART | METHOD を |
UTCに |
| DTEND | DURATION と |
開始+所要時間 |
| ORGANIZER | PUBLISH では |
式場の |
| ATTENDEE | PUBLISH では |
書かない |
| SEQUENCE | 0より |
予約時0、 |
METHOD を
1行75オクテットの折り返しは、日本語の文字の途中で切らない
RFC 5545 では、
RFC 5545 には、
// fair-ics.mjs(抜粋)
const CRLF = "\r\n";
const enc = new TextEncoder();
// TEXT の値のエスケープ(RFC 5545 3.3.11):\ ; , と改行
export function escapeText(s) {
return String(s)
.replace(/\\/g, "\\\\")
.replace(/;/g, "\\;")
.replace(/,/g, "\\,")
.replace(/\r\n|\r|\n/g, "\\n");
}
// 1行を75オクテット以内で折り返す(RFC 5545 3.1)。
// 文字の途中(UTF-8の複数バイトの途中)では切らない
export function fold(line) {
const out = [];
let cur = "";
let bytes = 0;
for (const ch of line) {
const n = enc.encode(ch).length;
const limit = out.length === 0 ? 75 : 74; // 2行目以降は先頭の空白1文字ぶんを引く
if (bytes + n > limit) {
out.push(cur);
cur = "";
bytes = 0;
}
cur += ch;
bytes += n;
}
out.push(cur);
return out.join(CRLF + " ");
}書き出したfor...of は
説明欄や\n で
開始時刻は、日本時間をUTCに直してZを付ける
日時は、DTSTART:20261114T010000Z です。
RFC 5545 では、
浮動時刻で
TZ=Asia/Tokyo
浮動 20261114T100000 → 2026/11/14 10:00:00(日本時間)
UTC 20261114T010000Z → 2026/11/14 10:00:00(日本時間)
TZ=Asia/Bangkok
浮動 20261114T100000 → 2026/11/14 12:00:00(日本時間)
UTC 20261114T010000Z → 2026/11/14 10:00:00(日本時間)
TZ=America/Los_Angeles
浮動 20261114T100000 → 2026/11/15 3:00:00(日本時間)
UTC 20261114T010000Z → 2026/11/14 10:00:00(日本時間)浮動時刻は、
TZID を
UIDは予約ごとのUUIDにして、変更はSEQUENCEを上げて送り直す
UIDは、
UIDに
RFC 5546 では、
$ node parse-check.mjs
1-booked.ics PUBLISH seq=0 CONFIRMED 2026/11/14 10:00:00 180分 "駐車場:あり(30台)"
2-changed.ics PUBLISH seq=1 CONFIRMED 2026/11/21 13:00:00 180分 "駐車場:あり(30台)"
3-cancelled.ics CANCEL seq=2 CANCELLED 2026/11/21 13:00:00 180分 "駐車場:あり(30台)"
$ python parse_check.py
1-booked.ics PUBLISH 0 CONFIRMED 2026-11-14 10:00 サンプル会場(〒000-0000 サンプル県サンプル市1-2-3)
2-changed.ics PUBLISH 1 CONFIRMED 2026-11-21 13:00 サンプル会場(〒000-0000 サンプル県サンプル市1-2-3)
3-cancelled.ics CANCEL 2 CANCELLED 2026-11-21 13:00 サンプル会場(〒000-0000 サンプル県サンプル市1-2-3)折り返した
ここで
確認メールには text/calendar の添付として付ける
確認メールにtext/calendar にし、method にcharset に
メールの
// mail.mjs(抜粋)
const mail = [
`From: ${mimeWord("サンプル会場 ブライダルサロン")} <[email protected]>`,
"To: [email protected]",
`Subject: ${mimeWord("【ご予約確認】ブライダルフェア 11月14日(土)10:00")}`,
"MIME-Version: 1.0",
`Content-Type: multipart/mixed; boundary="${boundary}"`,
"",
`--${boundary}`,
'Content-Type: text/plain; charset="UTF-8"',
"Content-Transfer-Encoding: base64",
"",
b64(body),
`--${boundary}`,
'Content-Type: text/calendar; charset="UTF-8"; method=PUBLISH',
'Content-Disposition: attachment; filename="fair.ics"',
"Content-Transfer-Encoding: base64",
"",
b64(ics),
`--${boundary}--`,
"",
].join("\r\n");$ python mail_check.py
Subject: 【ご予約確認】ブライダルフェア 11月14日(土)10:00
text/plain {'charset': 'UTF-8'} None
text/calendar {'charset': 'UTF-8', 'method': 'PUBLISH'} fair.ics
添付を取り出したら元の .ics と同じ: Trueファイル名はtext/calendar を
送る
予約完了ページからも、同じ .ics をダウンロードできるようにする
メールが
$ curl -s -D - -o /dev/null 'http://localhost:8931/fair/booking/calendar.ics?t=tkn_9c2f0d'
HTTP/1.1 200 OK
Content-Type: text/calendar; charset=utf-8
Content-Disposition: attachment; filename="fair.ics"
Cache-Control: private, no-store
X-Robots-Tag: noindex予約ごとのprivate, no-store)、X-Robots-Tag: noindex)ように
ダウンロードの
説明欄に入れるのは式場の案内だけにする
.ics の
説明欄に
- 所要時間と、
受付の 場所 (建物の 入口が 分かりにくい場合) - 駐車場の
有無、 最寄り駅からの 道順の 要点 - 日時の
変更と 取り消しの 方法 (予約確認ページか 電話) - 式場の
電話番号 ( 当日、 遅れそうな ときの 連絡先) - 予約確認ページの
URL (本人確認の トークン付き。 開くと 予約の 詳細が 見える)
予約確認ページの
実装の前に、式場側で決めておくこと
.ics の
| 決めること | 選択肢の例 | 決める理由 |
|---|---|---|
| 付ける |
フェア・ |
打ち合わせは |
| 予定の |
式場名を |
お客様の |
| 終了時刻 | フェアの |
お客様が |
| 変更・取り消しの |
送り直す/メール本文で |
送り直す |
| 電話で |
電話の |
電話の |
電話と
株式会社bundlyzeでは、
確かめたことと、確かめていないこと
ここまでの
- macOS 26.6.2、
Node.js 22.23.2 ( node:test)、Python 3 ( emailパッケージ)、curl 8.7.1 - 読み直しに
使った ライブラリ:ical.js 2.2.1、 icalendar 6.3.2 (Python) node --testで、次の 6件の テストが 通りました :改行が すべて CRLFである こと、 どの 行も 75オクテット以内である こと、 折り返しを 戻すと 日本語が 元どおりに なる こと、 TEXT の エスケープ、 日本時間10時の 開始が UTCの 01:00Zに なる こと、 変更で UIDが 変わらず SEQUENCEが 上がり、 取り消しが CANCEL と CANCELLED に なる こと
ok 1 - 改行はすべてCRLFで、単独のLFが無い
ok 2 - どの行も75オクテット以内
ok 3 - 折り返しを戻すと、日本語が元どおりになる(文字の途中で切っていない)
ok 4 - TEXTのエスケープ:\\ ; , 改行。コロンはそのまま
ok 5 - 日本時間10:00の開始は、UTCの01:00Zで書く
ok 6 - 日時の変更はUIDを変えずにSEQUENCEを上げる。取り消しはCANCELとCANCELLED確かめていない
- 実際の
メールの 送信と、 iPhoneの 標準の メール・Gmailの アプリ・Outlookなどで、 添付の .ics が どう 表示され、 どう 追加されるか - 2通目
(変更)で 前の 予定が 置き換わるか、 3通目 (取り消し)で 予定が 消えるか。 アプリに よって 違う 可能性が あります - 予約完了ページの
ボタンを、 スマホの ブラウザで 押した ときの 動き
出典
この
- RFC 5545
(iCalendar) :CRLFと75オクテットの 折り返し、 UTF-8の 文字の 途中で 折り返してしまう 実装への 注意 (3.1)、 時刻の 3つの 書き方と 浮動時刻 (3.3.5)、 TEXT の エスケープ (3.3.11)、 VCALENDAR の PRODID・VERSION、 VEVENT の UID・DTSTAMP と METHOD が ない ときの DTSTART、 DTEND と DURATION を 同時に 書かない こと (3.6・3.6.1)、 METHOD が ない 場合は 予定の やり取りと して 扱わない こと (3.7.2)、 TZID ごとの VTIMEZONE (3.6.5)、 DTSTAMP は UTC (3.8.7.2)、 拡張子 ics (8.1) - RFC 5546
(iTIP) :PUBLISH の意味と、 ORGANIZER は 必須・ATTENDEE は 書かない こと、 SEQUENCE、 PUBLISH で 配った 予定の 更新と CANCEL での 取り消し (3.2.1)、 REQUEST が 出席者を 招く 使い方である こと (3.2.2)、 CANCEL の 決まり (3.2.5) - RFC 6047
(iMIP) :Content-Type のmethod と charset、 ORGANIZER の mailto:、 Content-Disposition より Content-Type で 扱いを 決める こと (2.3・2.4・2.6) - RFC 7986:UID に
利用者や ドメインを 特定できる 情報を 入れない こと、 ランダムな UUIDを すすめる こと (5.3) - Google カレンダーに
予定を :パソコンの読み込む (Google カレンダー ヘルプ) 設定の 「インポート / エクスポート」から .ics を 読み込む 手順
よくある質問
予約確認メールに .ics を添付すれば、お客様のカレンダーに自動で予定が入りますか?
自動では
.ics の開始時刻は、日本時間のまま書いてよいですか?
タイムゾーンを
フェアの日時を変えたときは、新しい .ics を送ればよいですか?
同じ
.ics の説明欄に、お客様の名前や電話番号を入れてもよいですか?
入れない