Skip to content

Commit 99c5cfb

Browse files
odelcroimcalinghee
authored andcommitted
add filters for legacy email and legacy localpart (#7)
1 parent 44b198c commit 99c5cfb

File tree

6 files changed

+236
-0
lines changed

6 files changed

+236
-0
lines changed

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ mas-templates = { path = "./crates/templates/", version = "=0.17.0-rc.0" }
6262
mas-tower = { path = "./crates/tower/", version = "=0.17.0-rc.0" }
6363
oauth2-types = { path = "./crates/oauth2-types/", version = "=0.17.0-rc.0" }
6464
syn2mas = { path = "./crates/syn2mas", version = "=0.17.0-rc.0" }
65+
tchap = { path = "./crates/tchap", version = "=0.1.0" }
6566

6667
# OpenAPI schema generation and validation
6768
[workspace.dependencies.aide]

crates/handlers/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ mas-templates.workspace = true
8787
oauth2-types.workspace = true
8888
zxcvbn.workspace = true
8989

90+
tchap.workspace = true
91+
9092
[dev-dependencies]
9193
insta.workspace = true
9294
tracing-subscriber.workspace = true

crates/handlers/src/upstream_oauth2/template.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use minijinja::{
1111
Environment, Error, ErrorKind, Value,
1212
value::{Enumerator, Object},
1313
};
14+
use tchap;
1415

1516
/// Context passed to the attribute mapping template
1617
///
@@ -188,6 +189,16 @@ pub fn environment() -> Environment<'static> {
188189
env.add_filter("string", string);
189190
env.add_filter("from_json", from_json);
190191

192+
// Add Tchap-specific filters, this could be a generic config submitted
193+
// to upstream allowing all users to add their own filters without upstream code
194+
// modifications tester les fonctions async pour le reseau
195+
env.add_filter("email_to_display_name", |s: &str| {
196+
tchap::email_to_display_name(s)
197+
});
198+
env.add_filter("email_to_mxid_localpart", |s: &str| {
199+
tchap::email_to_mxid_localpart(s)
200+
});
201+
191202
env.set_unknown_method_callback(minijinja_contrib::pycompat::unknown_method_callback);
192203

193204
env

crates/tchap/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "tchap"
3+
version = "0.1.0"
4+
description = "Tchap-specific functionality for Matrix Authentication Service"
5+
license = "MIT"
6+
7+
[dependencies]

crates/tchap/src/lib.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// Copyright 2025 New Vector Ltd.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
// Please see LICENSE in the repository root for full details.
5+
6+
//! Tchap-specific functionality for Matrix Authentication Service
7+
8+
/// Capitalise parts of a name containing different words, including those
9+
/// separated by hyphens.
10+
///
11+
/// For example, 'John-Doe'
12+
///
13+
/// # Parameters
14+
///
15+
/// * `name`: The name to parse
16+
///
17+
/// # Returns
18+
///
19+
/// The capitalized name
20+
#[must_use]
21+
pub fn cap(name: &str) -> String {
22+
if name.is_empty() {
23+
return name.to_string();
24+
}
25+
26+
// Split the name by whitespace then hyphens, capitalizing each part then
27+
// joining it back together.
28+
let capitalized_name = name
29+
.split_whitespace()
30+
.map(|space_part| {
31+
space_part
32+
.split('-')
33+
.map(|part| {
34+
let mut chars = part.chars();
35+
match chars.next() {
36+
None => String::new(),
37+
Some(first_char) => {
38+
let first_char_upper = first_char.to_uppercase().collect::<String>();
39+
let rest: String = chars.collect();
40+
format!("{}{}", first_char_upper, rest)
41+
}
42+
}
43+
})
44+
.collect::<Vec<String>>()
45+
.join("-")
46+
})
47+
.collect::<Vec<String>>()
48+
.join(" ");
49+
50+
capitalized_name
51+
}
52+
53+
/// Generate a Matrix ID localpart from an email address.
54+
///
55+
/// This function:
56+
/// 1. Replaces "@" with "-" in the email address
57+
/// 2. Converts the email to lowercase
58+
/// 3. Filters out any characters that are not allowed in a Matrix ID localpart
59+
///
60+
/// The allowed characters are: lowercase ASCII letters, digits, and "_-./="
61+
///
62+
/// # Parameters
63+
///
64+
/// * `address`: The email address to process
65+
///
66+
/// # Returns
67+
///
68+
/// A valid Matrix ID localpart derived from the email address
69+
#[must_use]
70+
pub fn email_to_mxid_localpart(address: &str) -> String {
71+
// Define the allowed characters for a Matrix ID localpart
72+
const ALLOWED_CHARS: &str = "abcdefghijklmnopqrstuvwxyz0123456789_-./=";
73+
74+
// Replace "@" with "-" and convert to lowercase
75+
let processed = address.replace('@', "-").to_lowercase();
76+
77+
// Filter out any characters that are not allowed
78+
processed
79+
.chars()
80+
.filter(|c| ALLOWED_CHARS.contains(*c))
81+
.collect()
82+
}
83+
84+
/// Generate a display name from an email address based on specific rules.
85+
///
86+
/// This function:
87+
/// 1. Replaces dots with spaces in the username part
88+
/// 2. Determines the organization based on domain rules:
89+
/// - gouv.fr emails use the subdomain or "gouv" if none
90+
/// - other emails use the second-level domain
91+
/// 3. Returns a display name in the format "Username [Organization]"
92+
///
93+
/// # Parameters
94+
///
95+
/// * `address`: The email address to process
96+
///
97+
/// # Returns
98+
///
99+
/// The formatted display name
100+
#[must_use]
101+
pub fn email_to_display_name(address: &str) -> String {
102+
// Split the part before and after the @ in the email.
103+
// Replace all . with spaces in the first part
104+
let parts: Vec<&str> = address.split('@').collect();
105+
if parts.len() != 2 {
106+
return String::new();
107+
}
108+
109+
let username = parts[0].replace('.', " ");
110+
let domain = parts[1];
111+
112+
// Figure out which org this email address belongs to
113+
let domain_parts: Vec<&str> = domain.split('.').collect();
114+
115+
let org = if domain_parts.len() >= 2
116+
&& domain_parts[domain_parts.len() - 2] == "gouv"
117+
&& domain_parts[domain_parts.len() - 1] == "fr"
118+
{
119+
// Is this is a ...gouv.fr address, set the org to whatever is before
120+
// gouv.fr. If there isn't anything (a @gouv.fr email) simply mark their
121+
// org as "gouv"
122+
if domain_parts.len() > 2 {
123+
domain_parts[domain_parts.len() - 3]
124+
} else {
125+
"gouv"
126+
}
127+
} else if domain_parts.len() >= 2 {
128+
// Otherwise, mark their org as the email's second-level domain name
129+
domain_parts[domain_parts.len() - 2]
130+
} else {
131+
""
132+
};
133+
134+
// Format the display name
135+
format!("{} [{}]", cap(&username), cap(org))
136+
}
137+
138+
#[cfg(test)]
139+
mod tests {
140+
use super::*;
141+
142+
#[test]
143+
fn test_cap() {
144+
assert_eq!(cap("john"), "John");
145+
assert_eq!(cap("john-doe"), "John-Doe");
146+
assert_eq!(cap("john doe"), "John Doe");
147+
assert_eq!(cap("john-doe smith"), "John-Doe Smith");
148+
assert_eq!(cap(""), "");
149+
}
150+
151+
#[test]
152+
fn test_email_to_display_name() {
153+
// Test gouv.fr email with subdomain
154+
assert_eq!(
155+
email_to_display_name("[email protected]"),
156+
"Jane Smith [Example]"
157+
);
158+
159+
// Test gouv.fr email without subdomain
160+
assert_eq!(email_to_display_name("[email protected]"), "User [Gouv]");
161+
162+
// Test gouv.fr email with subdomain
163+
assert_eq!(
164+
email_to_display_name("[email protected]"),
165+
"User [Gendarmerie]"
166+
);
167+
168+
// Test gouv.fr email with subdomain
169+
assert_eq!(
170+
email_to_display_name("[email protected]"),
171+
"User [Interieur]"
172+
);
173+
174+
// Test regular email
175+
assert_eq!(
176+
email_to_display_name("[email protected]"),
177+
"Contact [Example]"
178+
);
179+
180+
// Test invalid email
181+
assert_eq!(email_to_display_name("invalid-email"), "");
182+
}
183+
184+
#[test]
185+
fn test_email_to_mxid_localpart() {
186+
// Test basic email
187+
assert_eq!(
188+
email_to_mxid_localpart("[email protected]"),
189+
"john.doe-example.com"
190+
);
191+
192+
// Test with uppercase letters
193+
assert_eq!(
194+
email_to_mxid_localpart("[email protected]"),
195+
"john.doe-example.com"
196+
);
197+
198+
// Test with special characters
199+
assert_eq!(
200+
email_to_mxid_localpart("[email protected]"),
201+
"usertag-domain.com"
202+
);
203+
204+
// Test with invalid characters
205+
assert_eq!(
206+
email_to_mxid_localpart("user!#$%^&*()@domain.com"),
207+
"user-domain.com"
208+
);
209+
}
210+
}

0 commit comments

Comments
 (0)