1use guerrillamail_client::{Client, ClientBuilder, EmailDetails, Error, Message};
2use std::cell::RefCell;
3use std::ffi::{CStr, c_char};
4use std::mem;
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::ptr;
7use std::time::Duration;
8use tokio::runtime::{Builder as RuntimeBuilder, Runtime};
9
10thread_local! {
11 static LAST_ERROR: RefCell<Option<Vec<u8>>> = const { RefCell::new(None) };
12}
13
14#[repr(C)]
15pub struct gm_builder_t {
16 _private: [u8; 0],
17}
18
19#[repr(C)]
20pub struct gm_client_t {
21 _private: [u8; 0],
22}
23
24#[repr(C)]
25#[allow(non_camel_case_types)]
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum gm_status_t {
28 GM_OK = 0,
29 GM_ERR_NULL = 1,
30 GM_ERR_INVALID_ARGUMENT = 2,
31 GM_ERR_REQUEST = 3,
32 GM_ERR_RESPONSE_PARSE = 4,
33 GM_ERR_TOKEN_PARSE = 5,
34 GM_ERR_JSON = 6,
35 GM_ERR_INTERNAL = 7,
36}
37
38#[repr(C)]
39#[derive(Debug)]
40pub struct gm_string_t {
41 pub ptr: *mut c_char,
42 pub len: usize,
43}
44
45#[repr(C)]
46#[derive(Debug)]
47pub struct gm_message_t {
48 pub mail_id: gm_string_t,
49 pub mail_from: gm_string_t,
50 pub mail_subject: gm_string_t,
51 pub mail_excerpt: gm_string_t,
52 pub mail_timestamp: gm_string_t,
53}
54
55#[repr(C)]
56#[derive(Debug)]
57pub struct gm_message_list_t {
58 pub ptr: *mut gm_message_t,
59 pub len: usize,
60}
61
62#[repr(C)]
63#[derive(Debug)]
64pub struct gm_email_details_t {
65 pub mail_id: gm_string_t,
66 pub mail_from: gm_string_t,
67 pub mail_subject: gm_string_t,
68 pub mail_body: gm_string_t,
69 pub mail_timestamp: gm_string_t,
70 pub attachment_count: u32,
71 pub has_attachment_count: bool,
72}
73
74impl Default for gm_string_t {
75 fn default() -> Self {
76 Self {
77 ptr: ptr::null_mut(),
78 len: 0,
79 }
80 }
81}
82
83impl Default for gm_message_list_t {
84 fn default() -> Self {
85 Self {
86 ptr: ptr::null_mut(),
87 len: 0,
88 }
89 }
90}
91
92#[derive(Clone, Debug)]
93struct BuilderState {
94 proxy: Option<String>,
95 danger_accept_invalid_certs: bool,
96 user_agent: Option<String>,
97 timeout_ms: u64,
98 #[cfg(test)]
99 ajax_url: Option<String>,
100 #[cfg(test)]
101 base_url: Option<String>,
102}
103
104impl Default for BuilderState {
105 fn default() -> Self {
106 Self {
107 proxy: None,
108 danger_accept_invalid_certs: true,
109 user_agent: None,
110 timeout_ms: 30_000,
111 #[cfg(test)]
112 ajax_url: None,
113 #[cfg(test)]
114 base_url: None,
115 }
116 }
117}
118
119impl BuilderState {
120 fn to_client_builder(&self) -> ClientBuilder {
121 let mut builder = Client::builder()
122 .danger_accept_invalid_certs(self.danger_accept_invalid_certs)
123 .timeout(Duration::from_millis(self.timeout_ms));
124
125 if let Some(proxy) = &self.proxy {
126 builder = builder.proxy(proxy.clone());
127 }
128 if let Some(user_agent) = &self.user_agent {
129 builder = builder.user_agent(user_agent.clone());
130 }
131 #[cfg(test)]
132 {
133 if let Some(ajax_url) = &self.ajax_url {
134 builder = builder.ajax_url(ajax_url.clone());
135 }
136 if let Some(base_url) = &self.base_url {
137 builder = builder.base_url(base_url.clone());
138 }
139 }
140 builder
141 }
142
143}
144
145struct ClientHandle {
146 runtime: Runtime,
147 client: Client,
148}
149
150fn with_ffi_boundary<F>(f: F) -> gm_status_t
151where
152 F: FnOnce() -> Result<(), gm_status_t>,
153{
154 clear_last_error();
155 match catch_unwind(AssertUnwindSafe(f)) {
156 Ok(Ok(())) => gm_status_t::GM_OK,
157 Ok(Err(status)) => status,
158 Err(_) => {
159 set_last_error("panic while executing FFI function");
160 gm_status_t::GM_ERR_INTERNAL
161 }
162 }
163}
164
165fn set_last_error(message: impl Into<String>) {
166 let mut bytes = message.into().into_bytes();
167 bytes.push(0);
168 LAST_ERROR.with(|cell| {
169 *cell.borrow_mut() = Some(bytes);
170 });
171}
172
173fn clear_last_error() {
174 LAST_ERROR.with(|cell| {
175 *cell.borrow_mut() = None;
176 });
177}
178
179fn status_from_error(error: Error) -> gm_status_t {
180 let status = match error {
181 Error::Request(_) => gm_status_t::GM_ERR_REQUEST,
182 Error::ResponseParse(_) => gm_status_t::GM_ERR_RESPONSE_PARSE,
183 Error::TokenParse => gm_status_t::GM_ERR_TOKEN_PARSE,
184 Error::Json(_) => gm_status_t::GM_ERR_JSON,
185 Error::Regex(_) | Error::HeaderValue(_) | Error::DomainParse => {
186 gm_status_t::GM_ERR_INTERNAL
187 }
188 };
189 set_last_error(error.to_string());
190 status
191}
192
193fn runtime_new() -> Result<Runtime, gm_status_t> {
194 RuntimeBuilder::new_multi_thread()
195 .enable_all()
196 .build()
197 .map_err(|error| {
198 set_last_error(format!("failed to create Tokio runtime: {error}"));
199 gm_status_t::GM_ERR_INTERNAL
200 })
201}
202
203fn null_error(message: &str) -> gm_status_t {
204 set_last_error(message);
205 gm_status_t::GM_ERR_NULL
206}
207
208fn invalid_arg(message: &str) -> gm_status_t {
209 set_last_error(message);
210 gm_status_t::GM_ERR_INVALID_ARGUMENT
211}
212
213unsafe fn builder_state_mut<'a>(
214 builder: *mut gm_builder_t,
215) -> Result<&'a mut BuilderState, gm_status_t> {
216 if builder.is_null() {
217 return Err(null_error("builder is null"));
218 }
219 Ok(unsafe { &mut *builder.cast::<BuilderState>() })
220}
221
222unsafe fn client_handle_ref<'a>(client: *mut gm_client_t) -> Result<&'a ClientHandle, gm_status_t> {
223 if client.is_null() {
224 return Err(null_error("client is null"));
225 }
226 Ok(unsafe { &*client.cast::<ClientHandle>() })
227}
228
229unsafe fn read_required_str(ptr: *const c_char, name: &str) -> Result<String, gm_status_t> {
230 if ptr.is_null() {
231 return Err(null_error(&format!("{name} is null")));
232 }
233 let value = unsafe { CStr::from_ptr(ptr) };
234 value
235 .to_str()
236 .map(str::to_owned)
237 .map_err(|_| invalid_arg(&format!("{name} is not valid UTF-8")))
238}
239
240fn into_gm_string(value: String) -> gm_string_t {
241 let mut bytes = value.into_bytes();
242 let len = bytes.len();
243 bytes.push(0);
244 let ptr = bytes.as_mut_ptr().cast::<c_char>();
245 mem::forget(bytes);
246 gm_string_t { ptr, len }
247}
248
249fn free_gm_string(value: &mut gm_string_t) {
250 if value.ptr.is_null() {
251 value.len = 0;
252 return;
253 }
254 let len_with_nul = value.len.saturating_add(1);
255 unsafe {
256 let _ = Vec::from_raw_parts(value.ptr.cast::<u8>(), len_with_nul, len_with_nul);
257 }
258 value.ptr = ptr::null_mut();
259 value.len = 0;
260}
261
262fn into_gm_message(message: Message) -> gm_message_t {
263 gm_message_t {
264 mail_id: into_gm_string(message.mail_id),
265 mail_from: into_gm_string(message.mail_from),
266 mail_subject: into_gm_string(message.mail_subject),
267 mail_excerpt: into_gm_string(message.mail_excerpt),
268 mail_timestamp: into_gm_string(message.mail_timestamp),
269 }
270}
271
272fn free_gm_message(message: &mut gm_message_t) {
273 free_gm_string(&mut message.mail_id);
274 free_gm_string(&mut message.mail_from);
275 free_gm_string(&mut message.mail_subject);
276 free_gm_string(&mut message.mail_excerpt);
277 free_gm_string(&mut message.mail_timestamp);
278}
279
280fn into_gm_email_details(details: EmailDetails) -> gm_email_details_t {
281 gm_email_details_t {
282 mail_id: into_gm_string(details.mail_id),
283 mail_from: into_gm_string(details.mail_from),
284 mail_subject: into_gm_string(details.mail_subject),
285 mail_body: into_gm_string(details.mail_body),
286 mail_timestamp: into_gm_string(details.mail_timestamp),
287 attachment_count: details.attachment_count.unwrap_or(0),
288 has_attachment_count: details.attachment_count.is_some(),
289 }
290}
291
292fn free_gm_email_details_fields(details: &mut gm_email_details_t) {
293 free_gm_string(&mut details.mail_id);
294 free_gm_string(&mut details.mail_from);
295 free_gm_string(&mut details.mail_subject);
296 free_gm_string(&mut details.mail_body);
297 free_gm_string(&mut details.mail_timestamp);
298 details.attachment_count = 0;
299 details.has_attachment_count = false;
300}
301
302fn build_client_handle(state: &BuilderState) -> Result<*mut gm_client_t, gm_status_t> {
303 let runtime = runtime_new()?;
304 let client = runtime
305 .block_on(state.to_client_builder().build())
306 .map_err(status_from_error)?;
307 let handle = ClientHandle { runtime, client };
308 Ok(Box::into_raw(Box::new(handle)).cast::<gm_client_t>())
309}
310
311#[unsafe(no_mangle)]
312pub extern "C" fn gm_builder_new(out_builder: *mut *mut gm_builder_t) -> gm_status_t {
313 with_ffi_boundary(|| {
314 if out_builder.is_null() {
315 return Err(null_error("out_builder is null"));
316 }
317 let builder = Box::new(BuilderState::default());
318 unsafe {
319 *out_builder = Box::into_raw(builder).cast::<gm_builder_t>();
320 }
321 Ok(())
322 })
323}
324
325#[unsafe(no_mangle)]
326pub extern "C" fn gm_builder_free(builder: *mut gm_builder_t) {
327 let _ = catch_unwind(AssertUnwindSafe(|| {
328 if builder.is_null() {
329 return;
330 }
331 unsafe {
332 let _ = Box::from_raw(builder.cast::<BuilderState>());
333 }
334 }));
335}
336
337#[unsafe(no_mangle)]
338pub extern "C" fn gm_builder_set_proxy(
339 builder: *mut gm_builder_t,
340 proxy: *const c_char,
341) -> gm_status_t {
342 with_ffi_boundary(|| {
343 let state = unsafe { builder_state_mut(builder)? };
344 if proxy.is_null() {
345 state.proxy = None;
346 return Ok(());
347 }
348 state.proxy = Some(unsafe { read_required_str(proxy, "proxy")? });
349 Ok(())
350 })
351}
352
353#[unsafe(no_mangle)]
354pub extern "C" fn gm_builder_set_danger_accept_invalid_certs(
355 builder: *mut gm_builder_t,
356 value: bool,
357) -> gm_status_t {
358 with_ffi_boundary(|| {
359 let state = unsafe { builder_state_mut(builder)? };
360 state.danger_accept_invalid_certs = value;
361 Ok(())
362 })
363}
364
365#[unsafe(no_mangle)]
366pub extern "C" fn gm_builder_set_user_agent(
367 builder: *mut gm_builder_t,
368 user_agent: *const c_char,
369) -> gm_status_t {
370 with_ffi_boundary(|| {
371 let state = unsafe { builder_state_mut(builder)? };
372 state.user_agent = Some(unsafe { read_required_str(user_agent, "user_agent")? });
373 Ok(())
374 })
375}
376
377#[unsafe(no_mangle)]
378pub extern "C" fn gm_builder_set_timeout_ms(
379 builder: *mut gm_builder_t,
380 timeout_ms: u64,
381) -> gm_status_t {
382 with_ffi_boundary(|| {
383 let state = unsafe { builder_state_mut(builder)? };
384 if timeout_ms == 0 {
385 return Err(invalid_arg("timeout_ms must be greater than zero"));
386 }
387 state.timeout_ms = timeout_ms;
388 Ok(())
389 })
390}
391
392#[unsafe(no_mangle)]
393pub extern "C" fn gm_builder_build(
394 builder: *mut gm_builder_t,
395 out_client: *mut *mut gm_client_t,
396) -> gm_status_t {
397 with_ffi_boundary(|| {
398 if out_client.is_null() {
399 return Err(null_error("out_client is null"));
400 }
401 let state = unsafe { builder_state_mut(builder)? };
402 let client = build_client_handle(state)?;
403 unsafe {
404 *out_client = client;
405 }
406 Ok(())
407 })
408}
409
410#[unsafe(no_mangle)]
411pub extern "C" fn gm_client_new_default(out_client: *mut *mut gm_client_t) -> gm_status_t {
412 with_ffi_boundary(|| {
413 if out_client.is_null() {
414 return Err(null_error("out_client is null"));
415 }
416 let state = BuilderState::default();
417 let client = build_client_handle(&state)?;
418 unsafe {
419 *out_client = client;
420 }
421 Ok(())
422 })
423}
424
425#[unsafe(no_mangle)]
426pub extern "C" fn gm_client_free(client: *mut gm_client_t) {
427 let _ = catch_unwind(AssertUnwindSafe(|| {
428 if client.is_null() {
429 return;
430 }
431 unsafe {
432 let _ = Box::from_raw(client.cast::<ClientHandle>());
433 }
434 }));
435}
436
437#[unsafe(no_mangle)]
438pub extern "C" fn gm_client_create_email(
439 client: *mut gm_client_t,
440 alias: *const c_char,
441 out_email: *mut gm_string_t,
442) -> gm_status_t {
443 with_ffi_boundary(|| {
444 if out_email.is_null() {
445 return Err(null_error("out_email is null"));
446 }
447 let handle = unsafe { client_handle_ref(client)? };
448 let alias = unsafe { read_required_str(alias, "alias")? };
449 let email = handle
450 .runtime
451 .block_on(handle.client.create_email(&alias))
452 .map_err(status_from_error)?;
453 unsafe {
454 *out_email = into_gm_string(email);
455 }
456 Ok(())
457 })
458}
459
460#[unsafe(no_mangle)]
461pub extern "C" fn gm_client_get_messages(
462 client: *mut gm_client_t,
463 email: *const c_char,
464 out_messages: *mut gm_message_list_t,
465) -> gm_status_t {
466 with_ffi_boundary(|| {
467 if out_messages.is_null() {
468 return Err(null_error("out_messages is null"));
469 }
470 let handle = unsafe { client_handle_ref(client)? };
471 let email = unsafe { read_required_str(email, "email")? };
472 let messages = handle
473 .runtime
474 .block_on(handle.client.get_messages(&email))
475 .map_err(status_from_error)?;
476 let boxed = messages
477 .into_iter()
478 .map(into_gm_message)
479 .collect::<Vec<_>>()
480 .into_boxed_slice();
481 let len = boxed.len();
482 let ptr = Box::into_raw(boxed).cast::<gm_message_t>();
483 unsafe {
484 (*out_messages).ptr = ptr;
485 (*out_messages).len = len;
486 }
487 Ok(())
488 })
489}
490
491#[unsafe(no_mangle)]
492pub extern "C" fn gm_client_fetch_email(
493 client: *mut gm_client_t,
494 email: *const c_char,
495 mail_id: *const c_char,
496 out_details: *mut *mut gm_email_details_t,
497) -> gm_status_t {
498 with_ffi_boundary(|| {
499 if out_details.is_null() {
500 return Err(null_error("out_details is null"));
501 }
502 let handle = unsafe { client_handle_ref(client)? };
503 let email = unsafe { read_required_str(email, "email")? };
504 let mail_id = unsafe { read_required_str(mail_id, "mail_id")? };
505 let details = handle
506 .runtime
507 .block_on(handle.client.fetch_email(&email, &mail_id))
508 .map_err(status_from_error)?;
509 let details = Box::new(into_gm_email_details(details));
510 unsafe {
511 *out_details = Box::into_raw(details);
512 }
513 Ok(())
514 })
515}
516
517#[unsafe(no_mangle)]
518pub extern "C" fn gm_client_delete_email(
519 client: *mut gm_client_t,
520 email: *const c_char,
521 out_deleted: *mut bool,
522) -> gm_status_t {
523 with_ffi_boundary(|| {
524 if out_deleted.is_null() {
525 return Err(null_error("out_deleted is null"));
526 }
527 let handle = unsafe { client_handle_ref(client)? };
528 let email = unsafe { read_required_str(email, "email")? };
529 let deleted = handle
530 .runtime
531 .block_on(handle.client.delete_email(&email))
532 .map_err(status_from_error)?;
533 unsafe {
534 *out_deleted = deleted;
535 }
536 Ok(())
537 })
538}
539
540#[unsafe(no_mangle)]
541pub extern "C" fn gm_string_free(value: *mut gm_string_t) {
542 let _ = catch_unwind(AssertUnwindSafe(|| {
543 if value.is_null() {
544 return;
545 }
546 let value = unsafe { &mut *value };
547 free_gm_string(value);
548 }));
549}
550
551#[unsafe(no_mangle)]
552pub extern "C" fn gm_message_list_free(messages: *mut gm_message_list_t) {
553 let _ = catch_unwind(AssertUnwindSafe(|| {
554 if messages.is_null() {
555 return;
556 }
557 let messages = unsafe { &mut *messages };
558 if !messages.ptr.is_null() {
559 let slice_ptr = ptr::slice_from_raw_parts_mut(messages.ptr, messages.len);
560 let mut boxed = unsafe { Box::from_raw(slice_ptr) };
561 for message in boxed.iter_mut() {
562 free_gm_message(message);
563 }
564 }
565 messages.ptr = ptr::null_mut();
566 messages.len = 0;
567 }));
568}
569
570#[unsafe(no_mangle)]
571pub extern "C" fn gm_email_details_free(details: *mut gm_email_details_t) {
572 let _ = catch_unwind(AssertUnwindSafe(|| {
573 if details.is_null() {
574 return;
575 }
576 let mut details = unsafe { Box::from_raw(details) };
577 free_gm_email_details_fields(&mut details);
578 }));
579}
580
581#[unsafe(no_mangle)]
582pub extern "C" fn gm_last_error_message() -> *const c_char {
583 LAST_ERROR.with(|cell| {
584 cell.borrow()
585 .as_ref()
586 .map(|bytes| bytes.as_ptr().cast::<c_char>())
587 .unwrap_or(ptr::null())
588 })
589}
590
591#[unsafe(no_mangle)]
592pub extern "C" fn gm_last_error_clear() {
593 clear_last_error();
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 fn c_string_ptr(value: &str) -> Vec<u8> {
600 let mut bytes = value.as_bytes().to_vec();
601 bytes.push(0);
602 bytes
603 }
604
605 #[test]
606 fn gm_string_free_is_null_safe() {
607 gm_string_free(ptr::null_mut());
608
609 let mut value = gm_string_t::default();
610 gm_string_free(&mut value);
611 assert!(value.ptr.is_null());
612 assert_eq!(value.len, 0);
613 }
614
615 #[test]
616 fn gm_builder_set_timeout_rejects_zero() {
617 let mut builder = ptr::null_mut();
618 assert_eq!(gm_builder_new(&mut builder), gm_status_t::GM_OK);
619
620 let status = gm_builder_set_timeout_ms(builder, 0);
621 assert_eq!(status, gm_status_t::GM_ERR_INVALID_ARGUMENT);
622 let error = unsafe { CStr::from_ptr(gm_last_error_message()) }
623 .to_str()
624 .expect("utf8 error");
625 assert!(error.contains("timeout_ms"));
626
627 gm_builder_free(builder);
628 }
629
630 #[test]
631 fn null_client_reports_error() {
632 let alias = c_string_ptr("demoalias");
633 let mut email = gm_string_t::default();
634 let status =
635 gm_client_create_email(ptr::null_mut(), alias.as_ptr().cast::<c_char>(), &mut email);
636 assert_eq!(status, gm_status_t::GM_ERR_NULL);
637 let error = unsafe { CStr::from_ptr(gm_last_error_message()) }
638 .to_str()
639 .expect("utf8 error");
640 assert!(error.contains("client"));
641 }
642}