@@ -65,13 +65,11 @@ function str_truthy_falsy(str)
6565 return false
6666end
6767
68- -- Parse recipients from inline code output or plain text
69- -- Supports multiple formats:
70- -- 1. Python list: ['a', 'b'] or ["a", "b"]
71- -- 2. R vector: "a" "b" "c"
72- -- 3. Comma-separated: a, b, c
73- -- 4. Line-separated: a\nb\nc
74- -- Returns an empty array if parsing fails
68+ -- Parse recipients using regex to find email addresses
69+ -- Matches pattern: local-part@domain.tld
70+ -- Handles any format: Python lists, R vectors, comma-separated,
71+ -- space-separated, quoted, unquoted, etc.
72+ -- Returns an empty array if no valid emails found
7573function parse_recipients (recipient_str )
7674 recipient_str = str_trunc_trim (recipient_str , 10000 )
7775
@@ -80,110 +78,26 @@ function parse_recipients(recipient_str)
8078 end
8179
8280 local recipients = {}
83-
84- -- Try Python list format ['...', '...'] or ["...", "..."]
85- if string.match (recipient_str , " ^%[" ) and string.match (recipient_str , " %]$" ) then
86- local content = string.sub (recipient_str , 2 , - 2 )
87-
88- -- Try to parse as Python/R list by splitting on commas
89- -- and stripping quotes and brackets from each item
90- recipients = {}
91- for item in string.gmatch (content , " [^,]+" ) do
92- local trimmed = str_trunc_trim (item , 1000 )
93- -- Strip leading/trailing brackets
94- trimmed = string.gsub (trimmed , " ^%[" , " " )
95- trimmed = string.gsub (trimmed , " %]$" , " " )
96- trimmed = str_trunc_trim (trimmed , 1000 )
97-
98- -- Strip leading/trailing quotes (ASCII single/double and UTF-8 curly quotes)
99- -- ASCII single quote '
100- trimmed = string.gsub (trimmed , " ^'" , " " )
101- trimmed = string.gsub (trimmed , " '$" , " " )
102- -- ASCII double quote "
103- trimmed = string.gsub (trimmed , ' ^"' , " " )
104- trimmed = string.gsub (trimmed , ' "$' , " " )
105- -- UTF-8 curly single quotes ' and ' (U+2018, U+2019)
106- trimmed = string.gsub (trimmed , " ^" .. string.char (226 , 128 , 152 ), " " )
107- trimmed = string.gsub (trimmed , string.char (226 , 128 , 153 ) .. " $" , " " )
108- -- UTF-8 curly double quotes " and " (U+201C, U+201D)
109- trimmed = string.gsub (trimmed , " ^" .. string.char (226 , 128 , 156 ), " " )
110- trimmed = string.gsub (trimmed , string.char (226 , 128 , 157 ) .. " $" , " " )
111-
112- trimmed = str_trunc_trim (trimmed , 1000 )
113- if trimmed ~= " " then
114- table.insert (recipients , trimmed )
115- end
116- end
117- if # recipients > 0 then
118- return recipients
119- end
120- end
121-
122- -- Try R-style quoted format (space-separated quoted strings outside of brackets)
123- recipients = {}
124- local found_any = false
125-
126- -- Try single quotes: 'a' 'b' 'c'
127- for quoted_pair in string.gmatch (recipient_str , " '([^']*)'" ) do
128- local trimmed = str_trunc_trim (quoted_pair , 1000 )
129- if trimmed ~= " " then
130- table.insert (recipients , trimmed )
131- found_any = true
81+ -- Match anything that's not a separator (quotes, commas, spaces, brackets, parens)
82+ -- This allows international characters while stopping at separators
83+ for email in string.gmatch (recipient_str , " [^%s,'\" %[%]%(%)]+@[^%s,'\" %[%]%(%)]+%.[^%s,'\" %[%]%(%)]+" ) do
84+ -- Strip any leading/trailing quote characters (both straight and curly)
85+ -- Straight quotes: ' "
86+ -- Curly single quotes: ' ' (U+2018, U+2019)
87+ -- Curly double quotes: " " (U+201C, U+201D)
88+ email = string.gsub (email , " ^['\" " .. string.char (226 , 128 , 152 ) .. string.char (226 , 128 , 153 ) .. string.char (226 , 128 , 156 ) .. string.char (226 , 128 , 157 ) .. " ]+" , " " )
89+ email = string.gsub (email , " ['\" " .. string.char (226 , 128 , 152 ) .. string.char (226 , 128 , 153 ) .. string.char (226 , 128 , 156 ) .. string.char (226 , 128 , 157 ) .. " ]+$" , " " )
90+
91+ if email ~= " " and string.match (email , " @" ) then
92+ table.insert (recipients , email )
13293 end
13394 end
134- if found_any then
135- return recipients
136- end
13795
138- -- Try double quotes: "a" "b" "c"
139- recipients = {}
140- for quoted_pair in string.gmatch (recipient_str , ' "([^"]*)"' ) do
141- local trimmed = str_trunc_trim (quoted_pair , 1000 )
142- if trimmed ~= " " then
143- table.insert (recipients , trimmed )
144- found_any = true
145- end
146- end
147- if found_any then
148- return recipients
149- end
150-
151- -- Try line-separated format (newlines or spaces)
152- -- Check if there are newlines or multiple space-separated emails
153- if string.match (recipient_str , " \n " ) or
154- (string.match (recipient_str , " @.*%s+.*@" ) and not string.match (recipient_str , " ," )) then
155- recipients = {}
156- -- Split on newlines or spaces
157- for item in string.gmatch (recipient_str , " [^\n %s]+" ) do
158- local trimmed = str_trunc_trim (item , 1000 )
159- if trimmed ~= " " and string.match (trimmed , " @" ) then
160- table.insert (recipients , trimmed )
161- found_any = true
162- end
163- end
164- if found_any then
165- return recipients
166- end
167- end
168-
169- -- Try comma-separated format without quotes
170- -- Split by comma and trim each part
171- recipients = {}
172- found_any = false
173- for part in string.gmatch (recipient_str , " [^,]+" ) do
174- local trimmed = str_trunc_trim (part , 1000 )
175- if trimmed ~= " " and not string.match (trimmed , " ^[%[%]]" ) then
176- table.insert (recipients , trimmed )
177- found_any = true
178- end
179- end
180- if found_any then
181- return recipients
96+ if # recipients == 0 then
97+ quarto .log .warning (" Could not parse recipients format: " .. recipient_str )
18298 end
18399
184- -- Could not parse - log warning and return empty
185- quarto .log .warning (" Could not parse recipients format: " .. recipient_str )
186- return {}
100+ return recipients
187101end
188102
189103local html_email_template_1 = [[
0 commit comments