1/* Header rename functions                              -*- mfl -*-
2   Copyright (C) 2010-2021 Sergey Poznyakoff
3
4   This program is free software; you can redistribute it and/or modify
5   it under the terms of the GNU General Public License as published by
6   the Free Software Foundation; either version 3, or (at your option)
7   any later version.
8
9   This program is distributed in the hope that it will be useful,
10   but WITHOUT ANY WARRANTY; without even the implied warranty of
11   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   GNU General Public License for more details.
13
14   You should have received a copy of the GNU General Public License
15   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17module 'header_rename'.
18require status
19
20func header_rename(string name, string newname; number idx)
21do
22  if not defined(idx)
23    set idx 1
24  fi
25  number msg current_message()
26  try
27  do
28    string value message_find_header(msg, name, idx)
29    header_delete(name, idx)
30    header_add(newname, value)
31  done
32  catch e_range
33  do
34    pass
35  done
36done
37
38/*
39 * Prefixes all headers with the given NAME with PREFIX.
40 * If PREFIX is not supplied, removes the matching headers.
41 */
42func header_prefix_all(string name; string prefix)
43do
44  number msg current_message()
45  loop for number i message_header_count(msg, name),
46       while i >= 1,
47       set i i - 1
48  do
49    if defined(prefix)
50      header_rename(name, prefix . name, i)
51    else
52      header_delete(name, i)
53    fi
54  done
55done
56
57/*
58 * header_prefix_pattern(string pattern; string prefix)
59 * ----------------------------------------------------
60 * Prefixes all headers that match PATTERN with PREFIX
61 *
62 * Implementing such functionality faces considerable difficulties because
63 * the milter API doesn't provide calls for iterating over all message
64 * headers.
65 * The basic algorithm is:
66 *   1. get the list of available messages from the captured message
67 *   2. for each header that matches pattern
68 *   2.1.  save its value
69 *   2.2.  delete that header
70 *   2.3.  insert new header with the name constructed as concatenation
71 *         or the prefix and original header name and value saved in 2.1
72 *
73 * The 2.3 should ideally preserve locations of the renamed headers.
74 * However, that is impossible using the Milter API.  It was consented then
75 * that it should preserve relative ordering of the renamed headers.
76 *
77 * The initial approach was to use header_add function.  Unfortunately, it
78 * follows whimsical rules when selecting the position where to insert the
79 * header.  Normally, headers are added to the end of the list.  However, if
80 * a header with such name already exists, the new one is added before it.
81 * The flags assigned to the header (H_USER and H_TRACE, in Sendmail) alter
82 * that decision.  Consequently, using header_add produces unjustifiably
83 * complex code.
84 *
85 * Then, there is header_insert.  Formally, it inserts the header at the given
86 * index in the header list.  However, the header list it operates on is not
87 * the list of headers as present in the message, but an opaque internal list
88 * maintained in MTA, whose contents is unknown to the filter and is updated
89 * after the filter finishes its job.  The only more or less reliable way of
90 * using this call is with index 0, which is supposed to add the header at
91 * the beginning of that internal list.  At least, this ensures that the
92 * relative ordering of the renamed headers is preserved.
93 * Obviously the renamed headers must be inserted in the reverse order.
94 *
95 * So, the modified algorithm is:
96 *   1. Build a temporary list of headers that match the pattern.  Each
97 *      element of the list contains the header name and its instance number.
98 *      The list is sorted in reverse order: the last header in the message
99 *      appears at the start of the list.
100 *      Here, the difficulty is the lack of dictionary data type in MFL, so
101 *      the list is built as colon-delimited list of header specifications.
102 *      Each specification is HDR/N, where HDR is the header name and N is
103 *      its instance number.
104 *   2. Iterate over that list using the `string_list_iterate' macro.  For
105 *      each element in the list
106 *   2.1.  save the header value
107 *   2.2.  delete that header
108 *   2.3.  insert the renamed header in position 0.
109 */
110#pragma regex push +extended
111func header_prefix_pattern(string pattern; string prefix)
112do
113  number msg current_message()
114  number count message_header_count(msg)
115  string h_list ':'
116
117  loop for number i 1,
118       while i <= count,
119       set i i + 1
120  do
121    string name message_nth_header_name(msg, i)
122    if name fnmatches pattern
123      if h_list matches ".*:%name/([[:digit:]]+):.*"
124	set h_list ":%name/" . (\1 + 1) . h_list
125      else
126        set h_list ":%name/1" . h_list
127      fi
128    fi
129  done
130
131  if h_list matches '^:(.+):$'
132    set h_list \1
133  fi
134
135  string_list_iterate(h_list, ":", h_spec, `
136  if h_spec matches "^(.+)/([[:digit:]]+)$"
137    string value message_find_header(msg, \1, \2)
138    header_delete(\1, \2)
139    if defined(prefix)
140      header_add(prefix . \1, value, 0)
141    fi
142  fi')
143done
144
145#pragma regex pop
146
147
148
149
150