1use super::{Macro, Variable, Variables};
8use crate::SystemTime;
9use crate::dns::{decimal_u8, hex_nibble};
10use std::{borrow::Cow, net::IpAddr};
11
12const DEFAULT_DELIMITERS: u64 = 1u64 << (b'.' - b'+');
13const LAST_DELIMITER: u8 = b'_' - b'+';
14
15impl Macro {
16 pub fn eval<'z, 'x: 'z>(
24 &'z self,
25 vars: &'x Variables<'x>,
26 default: &'x str,
27 fqdn: bool,
28 ) -> Cow<'z, str> {
29 match self {
30 Macro::Literal(literal) => std::str::from_utf8(literal).unwrap_or_default().into(),
31 Macro::Variable {
32 letter,
33 num_parts,
34 reverse,
35 escape,
36 delimiters,
37 } => match vars.get(*letter, *num_parts, *reverse, *escape, fqdn, *delimiters) {
38 Cow::Borrowed(bytes) => std::str::from_utf8(bytes).unwrap_or_default().into(),
39 Cow::Owned(bytes) => String::from_utf8(bytes).unwrap_or_default().into(),
40 },
41 Macro::List(list) => {
42 let mut result = Vec::with_capacity(
43 list.iter()
44 .map(|item| match item {
45 Macro::Literal(literal) => literal.len(),
46 _ => 24,
47 })
48 .sum::<usize>()
49 + 1,
50 );
51 for item in list {
52 match item {
53 Macro::Literal(literal) => {
54 result.extend_from_slice(literal);
55 }
56 Macro::Variable {
57 letter,
58 num_parts,
59 reverse,
60 escape,
61 delimiters,
62 } => {
63 vars.append(
64 &mut result,
65 *letter,
66 Transform {
67 num_parts: *num_parts,
68 reverse: *reverse,
69 escape: *escape,
70 fqdn: false,
71 delimiters: *delimiters,
72 },
73 );
74 }
75 Macro::List(_) | Macro::None => unreachable!(),
76 }
77 }
78 if fqdn && matches!(result.last(), Some(last) if *last != b'.') {
79 result.push(b'.');
80 }
81 String::from_utf8(result).unwrap_or_default().into()
82 }
83 Macro::None => default.into(),
84 }
85 }
86
87 pub fn needs_ptr(&self) -> bool {
90 match self {
91 Macro::Variable { letter, .. } => *letter == Variable::ValidatedDomain,
92 Macro::List(list) => list.iter().any(|m| matches!(m, Macro::Variable { letter, .. } if *letter == Variable::ValidatedDomain)),
93 _ => false,
94 }
95 }
96}
97
98impl<'x> Variables<'x> {
99 pub fn new() -> Self {
103 Variables {
104 current_time_on_demand: true,
105 ..Default::default()
106 }
107 }
108
109 pub fn set_ip(&mut self, value: &IpAddr) {
111 let (v, i, c): (&'static [u8], Vec<u8>, Vec<u8>) = match value {
112 IpAddr::V4(ip) => {
113 let mut dotted = Vec::with_capacity(15);
114 let mut buf = [0u8; 3];
115 for octet in ip.octets() {
116 if !dotted.is_empty() {
117 dotted.push(b'.');
118 }
119 dotted.extend_from_slice(decimal_u8(octet, &mut buf));
120 }
121 (b"in-addr", dotted.clone(), dotted)
122 }
123 IpAddr::V6(ip) => {
124 let mut segments = Vec::with_capacity(63);
125 for segment in ip.segments() {
126 for shift in [12u32, 8, 4, 0] {
127 if !segments.is_empty() {
128 segments.push(b'.');
129 }
130 segments.push(hex_nibble((segment >> shift) as u8));
131 }
132 }
133 (b"ip6", segments, ip.to_string().into_bytes())
134 }
135 };
136 self.vars[Variable::IpVersion as usize] = v.into();
137 self.vars[Variable::Ip as usize] = i.into();
138 self.vars[Variable::SmtpIp as usize] = c.into();
139 }
140
141 pub fn set_sender(&mut self, value: impl Into<Cow<'x, [u8]>>) {
144 let value = value.into();
145 for (pos, ch) in value.iter().enumerate() {
146 if ch == &b'@' {
147 if pos > 0 {
148 self.vars[Variable::SenderLocalPart as usize] = match &value {
149 Cow::Borrowed(value) => (&value[..pos]).into(),
150 Cow::Owned(value) => value[..pos].to_vec().into(),
151 };
152 }
153 self.vars[Variable::SenderDomainPart as usize] = match &value {
154 Cow::Borrowed(value) => (value.get(pos + 1..).unwrap_or_default()).into(),
155 Cow::Owned(value) => (value.get(pos + 1..).unwrap_or_default()).to_vec().into(),
156 };
157 break;
158 }
159 }
160
161 self.vars[Variable::Sender as usize] = value;
162 }
163
164 pub fn set_helo_domain(&mut self, value: impl Into<Cow<'x, [u8]>>) {
166 self.vars[Variable::HeloDomain as usize] = value.into();
167 }
168
169 pub fn set_host_domain(&mut self, value: impl Into<Cow<'x, [u8]>>) {
171 self.vars[Variable::HostDomain as usize] = value.into();
172 }
173
174 pub fn set_validated_domain(&mut self, value: impl Into<Cow<'x, [u8]>>) {
176 self.vars[Variable::ValidatedDomain as usize] = value.into();
177 }
178
179 pub fn set_domain(&mut self, value: impl Into<Cow<'x, [u8]>>) {
181 self.vars[Variable::Domain as usize] = value.into();
182 }
183
184 pub fn get(
194 &self,
195 name: Variable,
196 num_parts: u32,
197 reverse: bool,
198 escape: bool,
199 fqdn: bool,
200 delimiters: u64,
201 ) -> Cow<'_, [u8]> {
202 let transform = Transform {
203 num_parts,
204 reverse,
205 escape,
206 fqdn,
207 delimiters,
208 };
209 let var: &[u8] = self.vars[name as usize].as_ref();
210 if var.is_empty() && self.current_time_on_demand && matches!(name, Variable::CurrentTime) {
211 let now = current_time();
212 if transform.is_verbatim() {
213 return Cow::Owned(now);
214 }
215 let mut result = Vec::with_capacity(transform.capacity_for(&now));
216 append_transformed(&mut result, &now, transform);
217 return Cow::Owned(result);
218 }
219 if var.is_empty() || transform.is_verbatim() {
220 return Cow::Borrowed(var);
221 }
222
223 let mut result = Vec::with_capacity(transform.capacity_for(var));
224 append_transformed(&mut result, var, transform);
225 Cow::Owned(result)
226 }
227
228 fn append(&self, result: &mut Vec<u8>, name: Variable, transform: Transform) {
229 let var: &[u8] = self.vars[name as usize].as_ref();
230 if var.is_empty() && self.current_time_on_demand && matches!(name, Variable::CurrentTime) {
231 append_variable(result, ¤t_time(), transform);
232 } else {
233 append_variable(result, var, transform);
234 }
235 }
236}
237
238#[derive(Clone, Copy)]
239struct Transform {
240 num_parts: u32,
241 reverse: bool,
242 escape: bool,
243 fqdn: bool,
244 delimiters: u64,
245}
246
247impl Transform {
248 #[inline(always)]
249 fn is_verbatim(&self) -> bool {
250 self.num_parts == 0
251 && !self.reverse
252 && !self.escape
253 && self.delimiters == DEFAULT_DELIMITERS
254 }
255
256 #[inline(always)]
257 fn capacity_for(&self, var: &[u8]) -> usize {
258 if self.escape {
259 var.len() * 3 + 1
260 } else {
261 var.len() + 1
262 }
263 }
264
265 #[inline(always)]
266 fn is_delimiter(&self, ch: u8) -> bool {
267 let offset = ch.wrapping_sub(b'+');
268 offset <= LAST_DELIMITER && (self.delimiters & (1u64 << offset)) != 0
269 }
270}
271
272fn append_variable(result: &mut Vec<u8>, var: &[u8], transform: Transform) {
273 if var.is_empty() || transform.is_verbatim() {
274 result.extend_from_slice(var);
275 } else {
276 append_transformed(result, var, transform);
277 }
278}
279
280fn append_transformed(result: &mut Vec<u8>, var: &[u8], transform: Transform) {
281 let skipped = if transform.num_parts == 0 {
282 0
283 } else {
284 let total = 1 + var.iter().filter(|ch| transform.is_delimiter(**ch)).count();
285 total - std::cmp::min(total, transform.num_parts as usize)
286 };
287
288 let start = result.len();
289 if !transform.reverse {
290 for (pos, part) in var
291 .split(|ch| transform.is_delimiter(*ch))
292 .skip(skipped)
293 .enumerate()
294 {
295 add_part(result, part, pos, transform.escape);
296 }
297 } else {
298 for (pos, part) in var
299 .rsplit(|ch| transform.is_delimiter(*ch))
300 .skip(skipped)
301 .enumerate()
302 {
303 add_part(result, part, pos, transform.escape);
304 }
305 }
306 if transform.fqdn && !matches!(result.get(start..), Some([.., b'.'])) {
307 result.push(b'.');
308 }
309}
310
311fn current_time() -> Vec<u8> {
312 let mut seconds = SystemTime::now()
313 .duration_since(SystemTime::UNIX_EPOCH)
314 .map(|d| d.as_secs())
315 .unwrap_or(0);
316 let mut buf = [0u8; 20];
317 let mut len = 0;
318 for slot in buf.iter_mut().rev() {
319 *slot = b'0' + (seconds % 10) as u8;
320 len += 1;
321 seconds /= 10;
322 if seconds == 0 {
323 break;
324 }
325 }
326 buf[buf.len() - len..].to_vec()
327}
328
329#[inline(always)]
330fn add_part(result: &mut Vec<u8>, part: &[u8], pos: usize, escape: bool) {
331 if pos > 0 {
332 result.push(b'.');
333 }
334 if !escape {
335 result.extend_from_slice(part);
336 } else {
337 for &ch in part {
338 if ch.is_ascii_alphanumeric() || matches!(ch, b'-' | b'.' | b'_' | b'~') {
339 result.push(ch);
340 } else {
341 result.extend_from_slice(&[b'%', hex_nibble(ch >> 4), hex_nibble(ch)]);
342 }
343 }
344 }
345}
346
347#[cfg(test)]
348mod test {
349 use std::net::IpAddr;
350
351 use crate::spf::{Variables, parse::SPFParser};
352
353 #[test]
354 fn expand_macro() {
355 let mut vars = Variables::new();
356 vars.set_sender("strong-bad@email.example.com".as_bytes());
357 vars.set_ip(&"192.0.2.3".parse::<IpAddr>().unwrap());
358 vars.set_validated_domain("mx.example.org".as_bytes());
359 vars.set_domain("email.example.com".as_bytes());
360 vars.set_helo_domain("....".as_bytes());
361
362 for (macro_string, expansion) in [
363 ("%{s}", "strong-bad@email.example.com"),
364 ("%{o}", "email.example.com"),
365 ("%{d}", "email.example.com"),
366 ("%{d4}", "email.example.com"),
367 ("%{d3}", "email.example.com"),
368 ("%{d2}", "example.com"),
369 ("%{d1}", "com"),
370 ("%{dr}", "com.example.email"),
371 ("%{d2r}", "example.email"),
372 ("%{l}", "strong-bad"),
373 ("%{l-}", "strong.bad"),
374 ("%{lr}", "strong-bad"),
375 ("%{lr-}", "bad.strong"),
376 ("%{l1r-}", "strong"),
377 ("%{p1r}", "mx"),
378 ("%{h3r}", ".."),
379 (
380 "%{ir}.%{v}._spf.%{d2}",
381 "3.2.0.192.in-addr._spf.example.com",
382 ),
383 ("%{lr-}.lp._spf.%{d2}", "bad.strong.lp._spf.example.com"),
384 (
385 "%{lr-}.lp.%{ir}.%{v}._spf.%{d2}",
386 "bad.strong.lp.3.2.0.192.in-addr._spf.example.com",
387 ),
388 (
389 "%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}",
390 "3.2.0.192.in-addr.strong.lp._spf.example.com",
391 ),
392 (
393 "%{d2}.trusted-domains.example.net",
394 "example.com.trusted-domains.example.net",
395 ),
396 ] {
397 let (m, _) = macro_string.as_bytes().iter().macro_string(false).unwrap();
398 assert_eq!(m.eval(&vars, "", false), expansion, "{macro_string:?}");
399 }
400
401 let mut vars = Variables::new();
402 vars.set_sender("strong-bad@email.example.com".as_bytes());
403 vars.set_ip(&"2001:db8::cb01".parse::<IpAddr>().unwrap());
404 vars.set_validated_domain("mx.example.org".as_bytes());
405 vars.set_domain("email.example.com".as_bytes());
406
407 for (macro_string, expansion) in [
408 (
409 "%{ir}.%{v}._spf.%{d2}",
410 concat!(
411 "1.0.b.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.",
412 "0.0.0.0.0.8.b.d.0.1.0.0.2.ip6._spf.example.com"
413 ),
414 ),
415 ("%{c}", "2001:db8::cb01"),
416 (
417 "%{c} is not one of %{d}'s designated mail servers.",
418 "2001:db8::cb01 is not one of email.example.com's designated mail servers.",
419 ),
420 (
421 "See http://%{d}/why.html?s=%{S}&i=%{C}",
422 concat!(
423 "See http://email.example.com/why.html?",
424 "s=strong-bad%40email.example.com&i=2001%3adb8%3a%3acb01"
425 ),
426 ),
427 ] {
428 let (m, _) = macro_string.as_bytes().iter().macro_string(true).unwrap();
429 assert_eq!(m.eval(&vars, "", false), expansion, "{macro_string:?}");
430 }
431 }
432}