Skip to content

Commit 4591182

Browse files
committed
Add mailroom landing page
Replace the inline HTML stub in directory.rs with a proper landing page served from static/index.html. The page explains the mailroom concept (combined OHTTP relay + directory), links to documentation and the self-hosting guide, and includes a footer with version and commit hash. A build.rs emits the git commit hash at compile time so the footer stays current without manual updates.
1 parent 7928026 commit 4591182

4 files changed

Lines changed: 285 additions & 57 deletions

File tree

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
path: type:
9292
(builtins.match ".*nginx.conf.template$" path != null)
9393
|| (builtins.match ".*\\.mmdb$" path != null)
94+
|| (builtins.match ".*\\.html$" path != null)
9495
|| (craneLibVersions.msrv.filterCargoSources path type);
9596
name = "source";
9697
};

payjoin-mailroom/build.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use std::process::Command;
2+
3+
fn main() {
4+
// Emit the short git commit hash at build time.
5+
let commit = Command::new("git")
6+
.args(["rev-parse", "--short", "HEAD"])
7+
.output()
8+
.ok()
9+
.filter(|o| o.status.success())
10+
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
11+
.unwrap_or_else(|| "unknown".into());
12+
13+
println!("cargo:rustc-env=GIT_COMMIT={commit}");
14+
15+
// Re-run if HEAD changes (new commit).
16+
println!("cargo:rerun-if-changed=../.git/HEAD");
17+
}

payjoin-mailroom/src/directory.rs

Lines changed: 12 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -433,64 +433,19 @@ fn handle_peek<E: SendableError>(
433433
}
434434
}
435435

436+
fn landing_page_html() -> String {
437+
const TEMPLATE: &str = include_str!("../static/index.html");
438+
const VERSION: &str = env!("CARGO_PKG_VERSION");
439+
const COMMIT: &str = env!("GIT_COMMIT");
440+
TEMPLATE.replace("{{VERSION}}", VERSION).replace("{{COMMIT}}", COMMIT)
441+
}
442+
436443
async fn handle_directory_home_path() -> Result<Response<Body>, HandlerError> {
437-
let mut res = Response::new(empty());
438-
*res.status_mut() = StatusCode::OK;
439-
res.headers_mut().insert(CONTENT_TYPE, HeaderValue::from_static("text/html"));
440-
441-
let html = r#"
442-
<!DOCTYPE html>
443-
<html lang="en">
444-
<head>
445-
<meta charset="UTF-8">
446-
<title>Payjoin Directory</title>
447-
<style>
448-
body {
449-
background-color: #0f0f0f;
450-
color: #eaeaea;
451-
font-family: Manrope, sans-serif;
452-
padding: 2rem;
453-
display: flex;
454-
justify-content: center;
455-
align-items: center;
456-
height: 100vh;
457-
}
458-
.container {
459-
background: #1a1a1a;
460-
border: 1px solid #333;
461-
border-radius: 8px;
462-
padding: 2rem;
463-
box-shadow: 0 0 10px rgba(0, 170, 255, 0.2);
464-
text-align: center;
465-
}
466-
h1 {
467-
color: black;
468-
background-color: #C71585;
469-
margin-bottom: 1rem;
470-
padding: 0.5rem;
471-
border-radius: 4px;
472-
}
473-
p {
474-
color: #ccc;
475-
}
476-
a{
477-
color: #F75394;
478-
text-decoration: none;
479-
}
480-
</style>
481-
</head>
482-
<body>
483-
<div class="container">
484-
<h1>Payjoin Directory</h1>
485-
<p>The Payjoin Directory provides a rendezvous point for sender and receiver to meet. The directory stores Payjoin payloads to support asynchronous communication.</p>
486-
<p>Learn more about how asynchronous payjoin works here: <a href="https://payjoin.org/docs/how-it-works/payjoin-v2-bip-77">Payjoin V2</a></p>
487-
</div>
488-
</body>
489-
</html>
490-
"#;
491-
492-
*res.body_mut() = full(html);
493-
Ok(res)
444+
let html = landing_page_html();
445+
Ok(Response::builder()
446+
.status(StatusCode::OK)
447+
.header(CONTENT_TYPE, "text/html")
448+
.body(full(html))?)
494449
}
495450

496451
#[derive(Debug)]

payjoin-mailroom/static/index.html

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Payjoin Mailroom</title>
7+
<link
8+
rel="icon"
9+
type="image/svg+xml"
10+
href="data:image/svg+xml,%3Csvg viewBox='0 0 36 36' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M18 36C27.9411 36 36 27.9411 36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36ZM3.79975 9.77472C2.32541 12.2837 1.58824 15.0254 1.58824 18C1.58824 20.9616 2.31894 23.7034 3.78035 26.2253C5.25469 28.7343 7.24635 30.7259 9.75532 32.2003C12.2772 33.6746 15.0254 34.4118 18 34.4118C20.9616 34.4118 23.6969 33.6811 26.2059 32.2197C28.7278 30.7453 30.7259 28.7537 32.2003 26.2447C33.6746 23.7228 34.4118 20.9746 34.4118 18C34.4118 15.0384 33.6746 12.3031 32.2003 9.79412C30.7388 7.27222 28.7472 5.27409 26.2253 3.79975C23.7163 2.32541 20.9746 1.58824 18 1.58824C15.0384 1.58824 12.2966 2.32541 9.77472 3.79975C7.26575 5.26116 5.27409 7.25282 3.79975 9.77472ZM15.8079 15.7691C15.213 16.364 14.9155 17.1076 14.9155 18C14.9155 18.8794 15.213 19.6166 15.8079 20.2115C16.4157 20.8064 17.1464 21.1039 18 21.1039C18.8665 21.1039 19.5972 20.8064 20.1921 20.2115C20.8 19.6037 21.1039 18.8665 21.1039 18C21.1039 17.1335 20.8064 16.3963 20.2115 15.7885C19.6166 15.1677 18.8794 14.8573 18 14.8573C17.1335 14.8573 16.4028 15.1612 15.8079 15.7691ZM5.56508 25.1777C4.2718 22.9533 3.62516 20.5607 3.62516 18C3.62516 15.4652 4.2524 13.0985 5.50688 10.8999C6.7743 8.68836 8.52023 6.92303 10.7447 5.60388C12.9691 4.28473 15.3876 3.62516 18 3.62516C20.5866 3.62516 22.9921 4.27826 25.2165 5.58448C27.4539 6.8907 29.2063 8.64956 30.4737 10.8611C31.7541 13.0597 32.3942 15.4393 32.3942 18C32.3942 20.5866 31.7411 22.9856 30.4349 25.1971C29.1416 27.4086 27.3892 29.161 25.1777 30.4543C22.9662 31.7347 20.5736 32.3748 18 32.3748C15.4522 32.3748 13.0661 31.7282 10.8417 30.4349C8.61723 29.1416 6.85836 27.3892 5.56508 25.1777Z' fill='%23f75394'/%3E%3C/svg%3E"
11+
/>
12+
<style>
13+
:root {
14+
--magenta: #f75390;
15+
--magenta-dark: #c93265;
16+
--bg: #0e0e10;
17+
--surface: #18181c;
18+
--surface-border: #28282e;
19+
--text: #e4e4e8;
20+
--text-muted: #9a9aa0;
21+
--text-dim: #6e6e76;
22+
}
23+
24+
* {
25+
margin: 0;
26+
padding: 0;
27+
box-sizing: border-box;
28+
}
29+
30+
body {
31+
background: var(--bg);
32+
color: var(--text);
33+
font-family:
34+
system-ui,
35+
-apple-system,
36+
"Segoe UI",
37+
Roboto,
38+
sans-serif;
39+
line-height: 1.6;
40+
min-height: 100vh;
41+
display: flex;
42+
flex-direction: column;
43+
}
44+
45+
header {
46+
display: flex;
47+
align-items: center;
48+
gap: 0.5rem;
49+
padding: 1.25rem 2rem;
50+
border-bottom: 1px solid var(--surface-border);
51+
}
52+
53+
header svg {
54+
width: 24px;
55+
height: 24px;
56+
flex-shrink: 0;
57+
}
58+
59+
header span {
60+
font-size: 0.75rem;
61+
font-weight: 700;
62+
letter-spacing: 0.12em;
63+
text-transform: uppercase;
64+
color: var(--text-muted);
65+
}
66+
67+
main {
68+
flex: 1;
69+
display: flex;
70+
justify-content: center;
71+
align-items: center;
72+
padding: 2rem;
73+
}
74+
75+
.card {
76+
background: var(--surface);
77+
border: 1px solid var(--surface-border);
78+
border-radius: 12px;
79+
padding: 2.5rem;
80+
max-width: 640px;
81+
width: 100%;
82+
}
83+
84+
h1 {
85+
font-size: 1.75rem;
86+
font-weight: 700;
87+
margin-bottom: 0.75rem;
88+
}
89+
90+
.lead {
91+
font-size: 1rem;
92+
color: var(--text-muted);
93+
margin-bottom: 1.75rem;
94+
}
95+
96+
.components {
97+
display: grid;
98+
gap: 1rem;
99+
margin-bottom: 1.75rem;
100+
}
101+
102+
.component {
103+
background: var(--bg);
104+
border: 1px solid var(--surface-border);
105+
border-radius: 8px;
106+
padding: 1rem 1.25rem;
107+
}
108+
109+
.component h2 {
110+
font-size: 0.9rem;
111+
font-weight: 600;
112+
margin-bottom: 0.25rem;
113+
}
114+
115+
.component p {
116+
font-size: 0.85rem;
117+
color: var(--text-muted);
118+
}
119+
120+
.notice {
121+
font-size: 0.8rem;
122+
color: var(--text-dim);
123+
border-left: 2px solid var(--surface-border);
124+
padding-left: 0.75rem;
125+
margin-bottom: 1.75rem;
126+
}
127+
128+
.links {
129+
display: flex;
130+
flex-wrap: wrap;
131+
gap: 0.75rem;
132+
}
133+
134+
.links a {
135+
color: var(--magenta);
136+
text-decoration: none;
137+
font-size: 0.8rem;
138+
font-weight: 500;
139+
padding: 0.35rem 0.65rem;
140+
border: 1px solid var(--magenta);
141+
border-radius: 6px;
142+
transition:
143+
background 0.15s,
144+
color 0.15s;
145+
white-space: nowrap;
146+
}
147+
148+
.links a:hover {
149+
background: var(--magenta);
150+
color: var(--bg);
151+
}
152+
153+
a {
154+
color: var(--magenta);
155+
text-decoration: none;
156+
transition: color 0.15s;
157+
}
158+
159+
a:hover {
160+
color: var(--magenta-dark);
161+
}
162+
163+
footer {
164+
border-top: 1px solid var(--surface-border);
165+
padding: 1rem 2rem;
166+
display: flex;
167+
justify-content: center;
168+
gap: 1.5rem;
169+
font-size: 0.75rem;
170+
color: var(--text-dim);
171+
}
172+
173+
footer code {
174+
font-family:
175+
ui-monospace, "Cascadia Code", "Source Code Pro", monospace;
176+
}
177+
</style>
178+
</head>
179+
180+
<body>
181+
<header>
182+
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
183+
<path
184+
fill-rule="evenodd"
185+
clip-rule="evenodd"
186+
d="M18 36C27.9411 36 36 27.9411 36 18C36 8.05888 27.9411 0 18 0C8.05888 0 0 8.05888 0 18C0 27.9411 8.05888 36 18 36ZM3.79975 9.77472C2.32541 12.2837 1.58824 15.0254 1.58824 18C1.58824 20.9616 2.31894 23.7034 3.78035 26.2253C5.25469 28.7343 7.24635 30.7259 9.75532 32.2003C12.2772 33.6746 15.0254 34.4118 18 34.4118C20.9616 34.4118 23.6969 33.6811 26.2059 32.2197C28.7278 30.7453 30.7259 28.7537 32.2003 26.2447C33.6746 23.7228 34.4118 20.9746 34.4118 18C34.4118 15.0384 33.6746 12.3031 32.2003 9.79412C30.7388 7.27222 28.7472 5.27409 26.2253 3.79975C23.7163 2.32541 20.9746 1.58824 18 1.58824C15.0384 1.58824 12.2966 2.32541 9.77472 3.79975C7.26575 5.26116 5.27409 7.25282 3.79975 9.77472ZM15.8079 15.7691C15.213 16.364 14.9155 17.1076 14.9155 18C14.9155 18.8794 15.213 19.6166 15.8079 20.2115C16.4157 20.8064 17.1464 21.1039 18 21.1039C18.8665 21.1039 19.5972 20.8064 20.1921 20.2115C20.8 19.6037 21.1039 18.8665 21.1039 18C21.1039 17.1335 20.8064 16.3963 20.2115 15.7885C19.6166 15.1677 18.8794 14.8573 18 14.8573C17.1335 14.8573 16.4028 15.1612 15.8079 15.7691ZM5.56508 25.1777C4.2718 22.9533 3.62516 20.5607 3.62516 18C3.62516 15.4652 4.2524 13.0985 5.50688 10.8999C6.7743 8.68836 8.52023 6.92303 10.7447 5.60388C12.9691 4.28473 15.3876 3.62516 18 3.62516C20.5866 3.62516 22.9921 4.27826 25.2165 5.58448C27.4539 6.8907 29.2063 8.64956 30.4737 10.8611C31.7541 13.0597 32.3942 15.4393 32.3942 18C32.3942 20.5866 31.7411 22.9856 30.4349 25.1971C29.1416 27.4086 27.3892 29.161 25.1777 30.4543C22.9662 31.7347 20.5736 32.3748 18 32.3748C15.4522 32.3748 13.0661 31.7282 10.8417 30.4349C8.61723 29.1416 6.85836 27.3892 5.56508 25.1777Z"
187+
fill="#f75394"
188+
/>
189+
</svg>
190+
<span>Payjoin Mailroom</span>
191+
</header>
192+
193+
<main>
194+
<div class="card">
195+
<h1>What is this?</h1>
196+
<p class="lead">
197+
The payjoin mailroom is a lightweight binary that bundles the two
198+
server-side roles required by BIP 77 Async Payjoin: a directory and an
199+
OHTTP relay. Together, they let sender and receiver complete a payjoin
200+
without being online at the same time, while keeping network
201+
identities private.
202+
</p>
203+
204+
<div class="components">
205+
<div class="component">
206+
<h2>Payjoin Directory</h2>
207+
<p>
208+
A store-and-forward mailbox that holds small, ephemeral,
209+
end-to-end encrypted payloads for asynchronous payjoin.
210+
</p>
211+
</div>
212+
<div class="component">
213+
<h2>OHTTP Relay</h2>
214+
<p>
215+
An Oblivious HTTP proxy that separates client IP addresses from
216+
the directory, preventing it from correlating users with their
217+
network identity.
218+
</p>
219+
</div>
220+
</div>
221+
222+
<p class="notice">
223+
OHTTP privacy requires that these two roles be operated by different
224+
parties. The mailroom enforces self-loop detection: requests where the
225+
relay and directory resolve to the same mailroom are rejected. Wallets
226+
should always pick distinct mailrooms for their relay and directory.
227+
</p>
228+
229+
<div class="links">
230+
<a href="https://payjoin.org/docs/how-it-works/payjoin-v2-bip-77"
231+
>How Async Payjoin works</a
232+
>
233+
<a
234+
href="https://github.com/payjoin/rust-payjoin/tree/master/payjoin-mailroom#payjoin-mailroom"
235+
>Run your own mailroom</a
236+
>
237+
<a href="https://payjoindevkit.org/introduction/"
238+
>Add Payjoin to your wallet</a
239+
>
240+
</div>
241+
</div>
242+
</main>
243+
244+
<footer>
245+
<span><code>payjoin-mailroom-v{{VERSION}} @ {{COMMIT}}</code></span>
246+
<span
247+
>Copyright &copy;
248+
<script>
249+
document.write(new Date().getFullYear());
250+
</script>
251+
The Payjoin Developers
252+
</span>
253+
</footer>
254+
</body>
255+
</html>

0 commit comments

Comments
 (0)