Skip to main content

guerrillamail_client_c/
lib.rs

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}