LCOV - code coverage report
Current view: top level - tests/unit - test_mail_rules.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 346 346
Test Date: 2026-05-07 15:53:07 Functions: 100.0 % 22 22

            Line data    Source code
       1              : #include "test_helpers.h"
       2              : #include "mail_rules.h"
       3              : #include <fcntl.h>
       4              : #include <stdio.h>
       5              : #include <stdlib.h>
       6              : #include <string.h>
       7              : #include <unistd.h>
       8              : 
       9              : /* ── Helpers ─────────────────────────────────────────────────────── */
      10              : 
      11           17 : static MailRules *make_rules(void) {
      12           17 :     return calloc(1, sizeof(MailRules));
      13              : }
      14              : 
      15           17 : static MailRule *add_rule(MailRules *r, const char *name) {
      16           17 :     if (r->count >= r->cap) {
      17           16 :         int nc = r->cap ? r->cap * 2 : 4;
      18           16 :         MailRule *tmp = realloc(r->rules, (size_t)nc * sizeof(MailRule));
      19           16 :         if (!tmp) return NULL;
      20           16 :         r->rules = tmp;
      21           16 :         r->cap   = nc;
      22              :     }
      23           17 :     MailRule *rule = &r->rules[r->count++];
      24           17 :     memset(rule, 0, sizeof(*rule));
      25           17 :     rule->name = strdup(name);
      26           17 :     return rule;
      27              : }
      28              : 
      29              : /* ── Tests ───────────────────────────────────────────────────────── */
      30              : 
      31            1 : static void test_glob_match_basic(void) {
      32            1 :     MailRules *r = make_rules();
      33            1 :     MailRule  *rule = add_rule(r, "GitHub");
      34            1 :     rule->if_from = strdup("*@github.com");
      35            1 :     rule->then_add_label[rule->then_add_count++] = strdup("GitHub");
      36              : 
      37            1 :     char **add = NULL, **rm = NULL;
      38            1 :     int ac = 0, rc2 = 0;
      39            1 :     int fired = mail_rules_apply(r,
      40              :                                   "noreply@github.com", "PR review", NULL, "INBOX",
      41              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
      42            1 :     ASSERT(fired == 1, "rule should fire for *@github.com");
      43            1 :     ASSERT(ac == 1,    "should add 1 label");
      44            1 :     ASSERT(strcmp(add[0], "GitHub") == 0, "added label should be GitHub");
      45            1 :     ASSERT(rc2 == 0,   "should remove 0 labels");
      46              : 
      47            2 :     for (int i = 0; i < ac; i++) free(add[i]);
      48            1 :     free(add);
      49            1 :     mail_rules_free(r);
      50              : }
      51              : 
      52            1 : static void test_glob_no_match(void) {
      53            1 :     MailRules *r = make_rules();
      54            1 :     MailRule  *rule = add_rule(r, "GitHub");
      55            1 :     rule->if_from = strdup("*@github.com");
      56            1 :     rule->then_add_label[rule->then_add_count++] = strdup("GitHub");
      57              : 
      58            1 :     char **add = NULL, **rm = NULL;
      59            1 :     int ac = 0, rc2 = 0;
      60            1 :     int fired = mail_rules_apply(r,
      61              :                                   "user@example.com", "Hello", NULL, "INBOX",
      62              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
      63            1 :     ASSERT(fired == 0, "rule should NOT fire for non-github address");
      64            1 :     ASSERT(ac == 0,    "should add 0 labels");
      65              : 
      66            1 :     mail_rules_free(r);
      67              : }
      68              : 
      69            1 : static void test_subject_glob(void) {
      70            1 :     MailRules *r = make_rules();
      71            1 :     MailRule  *rule = add_rule(r, "Invoices");
      72            1 :     rule->if_subject = strdup("*invoice*");
      73            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Invoices");
      74              : 
      75            1 :     char **add = NULL, **rm = NULL;
      76            1 :     int ac = 0, rc2 = 0;
      77            1 :     int fired = mail_rules_apply(r,
      78              :                                   "billing@acme.com", "Invoice April 2026", NULL, "",
      79              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
      80            1 :     ASSERT(fired == 1, "subject glob should match 'Invoice April 2026'");
      81            1 :     ASSERT(ac == 1,    "should add Invoices label");
      82              : 
      83            2 :     for (int i = 0; i < ac; i++) free(add[i]);
      84            1 :     free(add);
      85            1 :     mail_rules_free(r);
      86              : }
      87              : 
      88            1 : static void test_case_insensitive(void) {
      89            1 :     MailRules *r = make_rules();
      90            1 :     MailRule  *rule = add_rule(r, "CI");
      91            1 :     rule->if_from = strdup("*@GITHUB.COM");
      92            1 :     rule->then_add_label[rule->then_add_count++] = strdup("CI");
      93              : 
      94            1 :     char **add = NULL, **rm = NULL;
      95            1 :     int ac = 0, rc2 = 0;
      96            1 :     int fired = mail_rules_apply(r,
      97              :                                   "noreply@github.com", "PR", NULL, "",
      98              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
      99            1 :     ASSERT(fired == 1, "glob match should be case-insensitive");
     100              : 
     101            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     102            1 :     free(add);
     103            1 :     mail_rules_free(r);
     104              : }
     105              : 
     106            1 : static void test_remove_label(void) {
     107            1 :     MailRules *r = make_rules();
     108            1 :     MailRule  *rule = add_rule(r, "Archive marketing");
     109            1 :     rule->if_from = strdup("*@marketing.example.com");
     110            1 :     rule->then_add_label[rule->then_add_count++]  = strdup("_spam");
     111            1 :     rule->then_rm_label[rule->then_rm_count++]    = strdup("INBOX");
     112              : 
     113            1 :     char **add = NULL, **rm = NULL;
     114            1 :     int ac = 0, rc2 = 0;
     115            1 :     int fired = mail_rules_apply(r,
     116              :                                   "promo@marketing.example.com", "Big Sale!", NULL,
     117              :                                   "INBOX,UNREAD",
     118              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     119            1 :     ASSERT(fired == 1, "rule should fire");
     120            1 :     ASSERT(ac == 1 && strcmp(add[0], "_spam") == 0, "should add _spam");
     121            1 :     ASSERT(rc2 == 1 && strcmp(rm[0], "INBOX") == 0, "should remove INBOX");
     122              : 
     123            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     124            2 :     for (int i = 0; i < rc2; i++) free(rm[i]);
     125            1 :     free(add); free(rm);
     126            1 :     mail_rules_free(r);
     127              : }
     128              : 
     129            1 : static void test_multiple_rules_chained(void) {
     130              :     /* Rule 1: if from @acme.com → add Client
     131              :      * Rule 2: if-label=Client  → add Priority, remove INBOX  */
     132            1 :     MailRules *r = make_rules();
     133              : 
     134            1 :     MailRule *r1 = add_rule(r, "Acme");
     135            1 :     r1->if_from = strdup("*@acme.com");
     136            1 :     r1->then_add_label[r1->then_add_count++] = strdup("Client");
     137              : 
     138            1 :     MailRule *r2 = add_rule(r, "Priority");
     139            1 :     r2->if_label = strdup("Client");
     140            1 :     r2->then_add_label[r2->then_add_count++] = strdup("Priority");
     141            1 :     r2->then_rm_label[r2->then_rm_count++]   = strdup("INBOX");
     142              : 
     143            1 :     char **add = NULL, **rm = NULL;
     144            1 :     int ac = 0, rc2 = 0;
     145            1 :     int fired = mail_rules_apply(r,
     146              :                                   "ceo@acme.com", "Quarterly results", NULL, "INBOX",
     147              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     148            1 :     ASSERT(fired == 2,   "both rules should fire");
     149            1 :     ASSERT(ac == 2,      "should add Client and Priority");
     150            1 :     ASSERT(rc2 == 1,     "should remove INBOX");
     151              : 
     152            1 :     int has_client = 0, has_priority = 0;
     153            3 :     for (int i = 0; i < ac; i++) {
     154            2 :         if (strcmp(add[i], "Client")   == 0) has_client   = 1;
     155            2 :         if (strcmp(add[i], "Priority") == 0) has_priority = 1;
     156            2 :         free(add[i]);
     157              :     }
     158            1 :     ASSERT(has_client,   "Client label should be added");
     159            1 :     ASSERT(has_priority, "Priority label should be added");
     160            2 :     for (int i = 0; i < rc2; i++) free(rm[i]);
     161            1 :     free(add); free(rm);
     162            1 :     mail_rules_free(r);
     163              : }
     164              : 
     165            1 : static void test_no_rules(void) {
     166            1 :     MailRules *r = make_rules();
     167            1 :     char **add = NULL, **rm = NULL;
     168            1 :     int ac = 0, rc2 = 0;
     169            1 :     int fired = mail_rules_apply(r, "x@y.com", "Hi", NULL, "",
     170              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     171            1 :     ASSERT(fired == 0, "empty rule set fires 0 rules");
     172            1 :     mail_rules_free(r);
     173              : }
     174              : 
     175            1 : static void test_no_condition_matches_all(void) {
     176              :     /* Rule with no conditions should match any message */
     177            1 :     MailRules *r = make_rules();
     178            1 :     MailRule  *rule = add_rule(r, "Catchall");
     179            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Processed");
     180              : 
     181            1 :     char **add = NULL, **rm = NULL;
     182            1 :     int ac = 0, rc2 = 0;
     183            1 :     int fired = mail_rules_apply(r, "any@example.com", "Whatever", NULL, "",
     184              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     185            1 :     ASSERT(fired == 1, "rule with no conditions should always fire");
     186            1 :     ASSERT(ac == 1,    "should add Processed label");
     187              : 
     188            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     189            1 :     free(add);
     190            1 :     mail_rules_free(r);
     191              : }
     192              : 
     193            1 : static void test_body_condition(void) {
     194            1 :     MailRules *r = make_rules();
     195            1 :     MailRule  *rule = add_rule(r, "Newsletter");
     196            1 :     rule->if_body = strdup("*unsubscribe*");
     197            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Newsletter");
     198              : 
     199            1 :     char **add = NULL, **rm = NULL;
     200            1 :     int ac = 0, rc2 = 0;
     201              : 
     202            1 :     int fired = mail_rules_apply(r, "news@example.com", "Weekly", NULL, "",
     203              :                                   "Please unsubscribe here if needed", (time_t)0,
     204              :                                   &add, &ac, &rm, &rc2);
     205            1 :     ASSERT(fired == 1, "if-body should match");
     206            1 :     ASSERT(ac == 1,    "should add Newsletter label");
     207            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     208            1 :     free(add); free(rm); add = NULL; rm = NULL; ac = 0; rc2 = 0;
     209              : 
     210            1 :     fired = mail_rules_apply(r, "news@example.com", "Weekly", NULL, "",
     211              :                               "Hello world", (time_t)0, &add, &ac, &rm, &rc2);
     212            1 :     ASSERT(fired == 0, "if-body should not match");
     213              : 
     214            1 :     fired = mail_rules_apply(r, "news@example.com", "Weekly", NULL, "",
     215              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     216            1 :     ASSERT(fired == 0, "if-body with NULL body should not fire");
     217              : 
     218            1 :     mail_rules_free(r);
     219              : }
     220              : 
     221            1 : static void test_age_condition(void) {
     222            1 :     MailRules *r = make_rules();
     223            1 :     MailRule  *rule = add_rule(r, "Old");
     224            1 :     rule->if_age_gt = 30;
     225            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Old");
     226              : 
     227            1 :     char **add = NULL, **rm = NULL;
     228            1 :     int ac = 0, rc2 = 0;
     229              : 
     230            1 :     time_t now     = time(NULL);
     231            1 :     time_t recent  = now - (1  * 86400);
     232            1 :     time_t old_msg = now - (60 * 86400);
     233              : 
     234            1 :     int fired = mail_rules_apply(r, "x@y.com", "Hi", NULL, "",
     235              :                                   NULL, recent, &add, &ac, &rm, &rc2);
     236            1 :     ASSERT(fired == 0, "if-age-gt=30 should not fire for 1-day-old message");
     237              : 
     238            1 :     fired = mail_rules_apply(r, "x@y.com", "Hi", NULL, "",
     239              :                               NULL, old_msg, &add, &ac, &rm, &rc2);
     240            1 :     ASSERT(fired == 1, "if-age-gt=30 should fire for 60-day-old message");
     241            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     242            1 :     free(add); free(rm); add = NULL; rm = NULL; ac = 0; rc2 = 0;
     243              : 
     244            1 :     fired = mail_rules_apply(r, "x@y.com", "Hi", NULL, "",
     245              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     246            1 :     ASSERT(fired == 1, "if-age-gt with unknown date should fire (age check skipped)");
     247            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     248            1 :     free(add); free(rm);
     249              : 
     250            1 :     mail_rules_free(r);
     251              : }
     252              : 
     253            1 : static void test_age_lt_condition(void) {
     254            1 :     MailRules *r = make_rules();
     255            1 :     MailRule  *rule = add_rule(r, "Recent");
     256            1 :     rule->if_age_lt = 7;
     257            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Recent");
     258              : 
     259            1 :     char **add = NULL, **rm = NULL;
     260            1 :     int ac = 0, rc2 = 0;
     261              : 
     262            1 :     time_t now     = time(NULL);
     263            1 :     time_t new_msg = now - (1  * 86400);
     264            1 :     time_t old_msg = now - (30 * 86400);
     265              : 
     266            1 :     int fired = mail_rules_apply(r, "x@y.com", "Hi", NULL, "",
     267              :                                   NULL, new_msg, &add, &ac, &rm, &rc2);
     268            1 :     ASSERT(fired == 1, "if-age-lt=7 should fire for 1-day-old message");
     269            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     270            1 :     free(add); free(rm); add = NULL; rm = NULL; ac = 0; rc2 = 0;
     271              : 
     272            1 :     fired = mail_rules_apply(r, "x@y.com", "Hi", NULL, "",
     273              :                               NULL, old_msg, &add, &ac, &rm, &rc2);
     274            1 :     ASSERT(fired == 0, "if-age-lt=7 should not fire for 30-day-old message");
     275              : 
     276            1 :     mail_rules_free(r);
     277              : }
     278              : 
     279            1 : static void test_negation_if_not_from(void) {
     280            1 :     MailRules *r = make_rules();
     281            1 :     MailRule  *rule = add_rule(r, "Not spam");
     282            1 :     rule->if_not_from = strdup("*@spam.example.com*");
     283            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Legit");
     284              : 
     285            1 :     char **add = NULL, **rm = NULL;
     286            1 :     int ac = 0, rc2 = 0;
     287              : 
     288              :     /* Should NOT match spam sender */
     289            1 :     int fired = mail_rules_apply(r, "bad@spam.example.com", "Hi", NULL, "",
     290              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     291            1 :     ASSERT(fired == 0, "if-not-from should reject matching sender");
     292              : 
     293              :     /* Should match non-spam sender */
     294            1 :     fired = mail_rules_apply(r, "good@legit.example.com", "Hi", NULL, "",
     295              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     296            1 :     ASSERT(fired == 1, "if-not-from should pass non-matching sender");
     297            1 :     ASSERT(ac == 1,    "should add Legit label");
     298            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     299            1 :     free(add); free(rm);
     300            1 :     mail_rules_free(r);
     301              : }
     302              : 
     303            1 : static void test_negation_if_not_subject(void) {
     304            1 :     MailRules *r = make_rules();
     305            1 :     MailRule  *rule = add_rule(r, "Not newsletter");
     306            1 :     rule->if_not_subject = strdup("*newsletter*");
     307            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Regular");
     308              : 
     309            1 :     char **add = NULL, **rm = NULL;
     310            1 :     int ac = 0, rc2 = 0;
     311              : 
     312            1 :     int fired = mail_rules_apply(r, "x@y.com", "Weekly newsletter digest", NULL, "",
     313              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     314            1 :     ASSERT(fired == 0, "if-not-subject should reject matching subject");
     315              : 
     316            1 :     fired = mail_rules_apply(r, "x@y.com", "Hello world", NULL, "",
     317              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     318            1 :     ASSERT(fired == 1, "if-not-subject should pass non-matching subject");
     319            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     320            1 :     free(add); free(rm);
     321            1 :     mail_rules_free(r);
     322              : }
     323              : 
     324            1 : static void test_negation_combined_with_positive(void) {
     325              :     /* if-from = *@legit.com AND if-not-subject = *spam* */
     326            1 :     MailRules *r = make_rules();
     327            1 :     MailRule  *rule = add_rule(r, "Legit non-spam");
     328            1 :     rule->if_from        = strdup("*@legit.com*");
     329            1 :     rule->if_not_subject = strdup("*spam*");
     330            1 :     rule->then_add_label[rule->then_add_count++] = strdup("OK");
     331              : 
     332            1 :     char **add = NULL, **rm = NULL;
     333            1 :     int ac = 0, rc2 = 0;
     334              : 
     335              :     /* Matches: legit domain, non-spam subject */
     336            1 :     int fired = mail_rules_apply(r, "user@legit.com", "Hello", NULL, "",
     337              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     338            1 :     ASSERT(fired == 1, "positive+negation: should match");
     339            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     340            1 :     free(add); free(rm); add = NULL; rm = NULL; ac = 0; rc2 = 0;
     341              : 
     342              :     /* Fails positive: wrong domain */
     343            1 :     fired = mail_rules_apply(r, "user@other.com", "Hello", NULL, "",
     344              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     345            1 :     ASSERT(fired == 0, "positive+negation: wrong domain should not match");
     346              : 
     347              :     /* Fails negation: spam subject */
     348            1 :     fired = mail_rules_apply(r, "user@legit.com", "Big spam offer", NULL, "",
     349              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     350            1 :     ASSERT(fired == 0, "positive+negation: spam subject should not match");
     351            1 :     mail_rules_free(r);
     352              : }
     353              : 
     354            1 : static void test_load_utf7_folder_name(void) {
     355              :     /* Write a temp rules.ini with a then-move-folder in IMAP modified UTF-7 */
     356            1 :     char tmppath[] = "/tmp/test-mail-rules-XXXXXX";
     357            1 :     int fd = mkstemp(tmppath);
     358            1 :     ASSERT(fd >= 0, "utf7 load: mkstemp failed");
     359            1 :     const char *ini =
     360              :         "[rule \"hivataos\"]\n"
     361              :         "if-from = *@gov.hu*\n"
     362              :         /* "hivatalos és pénzügy" in IMAP modified UTF-7 */
     363              :         "then-move-folder = hivatalos &AOk-s p&AOk-nz&APw-gy\n";
     364            1 :     ssize_t written = write(fd, ini, strlen(ini));
     365              :     (void)written;
     366            1 :     close(fd);
     367              : 
     368            1 :     MailRules *r = mail_rules_load_path(tmppath);
     369            1 :     unlink(tmppath);
     370            1 :     ASSERT(r != NULL, "utf7 load: load_path returned NULL");
     371            1 :     ASSERT(r->count == 1, "utf7 load: expected 1 rule");
     372            1 :     ASSERT(r->rules[0].then_move_folder != NULL, "utf7 load: then_move_folder is NULL");
     373              :     /* é = U+00E9 = C3 A9; ü = U+00FC = C3 BC */
     374            1 :     ASSERT(strcmp(r->rules[0].then_move_folder,
     375              :                   "hivatalos \xC3\xA9s p\xC3\xA9nz\xC3\xBCgy") == 0,
     376              :            "utf7 load: then_move_folder not decoded to UTF-8");
     377            1 :     mail_rules_free(r);
     378              : }
     379              : 
     380            1 : static void test_rules_parse_file_features(void) {
     381              :     /* Rules file with unnamed rule, when=, then-add/remove-label, then-forward-to */
     382            1 :     char tmppath[] = "/tmp/test-mail-rules-features-XXXXXX";
     383            1 :     int fd = mkstemp(tmppath);
     384            1 :     ASSERT(fd >= 0, "parse features: mkstemp failed");
     385              : 
     386            1 :     const char *ini =
     387              :         "[rule]\n"
     388              :         "when = from:*@test.com\n"
     389              :         "then-add-label    = TestLabel\n"
     390              :         "then-remove-label = INBOX\n"
     391              :         "then-forward-to   = fwd@example.com\n";
     392            1 :     ssize_t w = write(fd, ini, strlen(ini)); (void)w;
     393            1 :     close(fd);
     394              : 
     395            1 :     MailRules *r = mail_rules_load_path(tmppath);
     396            1 :     unlink(tmppath);
     397              : 
     398            1 :     ASSERT(r != NULL, "parse features: load returned non-NULL");
     399            1 :     ASSERT(r->count == 1, "parse features: 1 rule loaded");
     400            1 :     ASSERT(r->rules[0].name != NULL, "parse features: name not NULL");
     401            1 :     ASSERT(strcmp(r->rules[0].name, "(unnamed)") == 0, "parse features: unnamed rule name");
     402            1 :     ASSERT(r->rules[0].when != NULL, "parse features: when set");
     403            1 :     ASSERT(strcmp(r->rules[0].when, "from:*@test.com") == 0, "parse features: when value");
     404            1 :     ASSERT(r->rules[0].then_add_count == 1, "parse features: then_add_count=1");
     405            1 :     ASSERT(strcmp(r->rules[0].then_add_label[0], "TestLabel") == 0, "parse features: then_add_label");
     406            1 :     ASSERT(r->rules[0].then_rm_count == 1, "parse features: then_rm_count=1");
     407            1 :     ASSERT(strcmp(r->rules[0].then_rm_label[0], "INBOX") == 0, "parse features: then_rm_label");
     408            1 :     ASSERT(r->rules[0].then_forward_to != NULL, "parse features: then_forward_to set");
     409            1 :     ASSERT(strcmp(r->rules[0].then_forward_to, "fwd@example.com") == 0,
     410              :            "parse features: then_forward_to value");
     411            1 :     mail_rules_free(r);
     412              : }
     413              : 
     414            1 : static void test_rules_when_expression(void) {
     415            1 :     MailRules *r = make_rules();
     416            1 :     MailRule  *rule = add_rule(r, "When test");
     417            1 :     rule->when = strdup("from:*@corp.com");
     418            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Corp");
     419              : 
     420            1 :     char **add = NULL, **rm = NULL;
     421            1 :     int ac = 0, rc2 = 0;
     422              : 
     423            1 :     int fired = mail_rules_apply(r, "boss@corp.com", "Hello", NULL, "",
     424              :                                   NULL, (time_t)0, &add, &ac, &rm, &rc2);
     425            1 :     ASSERT(fired == 1, "when expr: fires for matching from");
     426            1 :     ASSERT(ac == 1, "when expr: adds 1 label");
     427            2 :     for (int i = 0; i < ac; i++) free(add[i]);
     428            1 :     free(add); free(rm); add = NULL; rm = NULL; ac = 0; rc2 = 0;
     429              : 
     430            1 :     fired = mail_rules_apply(r, "other@example.com", "Hi", NULL, "",
     431              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     432            1 :     ASSERT(fired == 0, "when expr: does not fire for non-matching from");
     433              : 
     434              :     /* Invalid when expression → rule skipped */
     435            1 :     free(rule->when);
     436            1 :     rule->when = strdup("!!!invalid!!!");
     437            1 :     fired = mail_rules_apply(r, "boss@corp.com", "Hello", NULL, "",
     438              :                               NULL, (time_t)0, &add, &ac, &rm, &rc2);
     439            1 :     ASSERT(fired == 0, "when expr: invalid expr → rule skipped");
     440              : 
     441            1 :     mail_rules_free(r);
     442              : }
     443              : 
     444            1 : static void test_rules_then_move_folder(void) {
     445            1 :     MailRules *r = make_rules();
     446            1 :     MailRule  *rule = add_rule(r, "Move Rule");
     447            1 :     rule->if_from = strdup("*@move.test");
     448            1 :     rule->then_move_folder = strdup("Processed");
     449              : 
     450            1 :     char **add = NULL, **rm = NULL;
     451            1 :     char *move_folder = NULL;
     452            1 :     int ac = 0, rc2 = 0;
     453              : 
     454            1 :     int fired = mail_rules_apply_ex(r, "x@move.test", "Msg", NULL, "",
     455              :                                      NULL, (time_t)0, &add, &ac, &rm, &rc2,
     456              :                                      &move_folder);
     457            1 :     ASSERT(fired == 1, "move_folder rule: fired");
     458            1 :     ASSERT(move_folder != NULL, "move_folder rule: move_folder set");
     459            1 :     if (move_folder)
     460            1 :         ASSERT(strcmp(move_folder, "Processed") == 0, "move_folder rule: value");
     461            1 :     free(move_folder);
     462            1 :     mail_rules_free(r);
     463              : }
     464              : 
     465            1 : static void test_rules_save_load_account(void) {
     466            1 :     char *old_home = getenv("HOME");
     467            1 :     char *old_xdg  = getenv("XDG_CONFIG_HOME");
     468            1 :     setenv("HOME", "/tmp/email-cli-rules-acct-test", 1);
     469            1 :     unsetenv("XDG_CONFIG_HOME");
     470              : 
     471            1 :     MailRules *r = make_rules();
     472            1 :     MailRule  *rule = add_rule(r, "Save Rule");
     473            1 :     rule->if_from = strdup("*@save.test");
     474            1 :     rule->when    = strdup("from:*@save.test");
     475            1 :     rule->then_add_label[rule->then_add_count++] = strdup("Saved");
     476            1 :     rule->then_rm_label[rule->then_rm_count++]   = strdup("INBOX");
     477            1 :     rule->then_move_folder = strdup("Archive");
     478              : 
     479            1 :     int rc = mail_rules_save("save-test@save.test", r);
     480            1 :     ASSERT(rc == 0, "rules_save: returns 0");
     481            1 :     mail_rules_free(r);
     482              : 
     483            1 :     MailRules *loaded = mail_rules_load("save-test@save.test");
     484            1 :     ASSERT(loaded != NULL, "rules_load: not NULL");
     485            1 :     if (loaded) {
     486            1 :         ASSERT(loaded->count == 1, "rules_load: count=1");
     487            1 :         ASSERT(loaded->rules[0].when != NULL, "rules_load: when set");
     488            1 :         if (loaded->rules[0].when)
     489            1 :             ASSERT(strcmp(loaded->rules[0].when, "from:*@save.test") == 0,
     490              :                    "rules_load: when value");
     491            1 :         ASSERT(loaded->rules[0].then_add_count == 1, "rules_load: then_add_count");
     492            1 :         ASSERT(loaded->rules[0].then_rm_count  == 1, "rules_load: then_rm_count");
     493            1 :         ASSERT(loaded->rules[0].then_move_folder != NULL, "rules_load: move_folder");
     494            1 :         mail_rules_free(loaded);
     495              :     }
     496              : 
     497              :     /* Load non-existent account → NULL */
     498            1 :     MailRules *miss = mail_rules_load("nobody@nowhere.invalid");
     499            1 :     ASSERT(miss == NULL, "rules_load: missing account returns NULL");
     500              : 
     501            1 :     if (old_home) setenv("HOME", old_home, 1); else unsetenv("HOME");
     502            1 :     if (old_xdg)  setenv("XDG_CONFIG_HOME", old_xdg, 1); else unsetenv("XDG_CONFIG_HOME");
     503              : }
     504              : 
     505              : /* ── Registration ────────────────────────────────────────────────── */
     506              : 
     507            1 : void test_mail_rules(void) {
     508            1 :     RUN_TEST(test_glob_match_basic);
     509            1 :     RUN_TEST(test_glob_no_match);
     510            1 :     RUN_TEST(test_subject_glob);
     511            1 :     RUN_TEST(test_case_insensitive);
     512            1 :     RUN_TEST(test_remove_label);
     513            1 :     RUN_TEST(test_multiple_rules_chained);
     514            1 :     RUN_TEST(test_no_rules);
     515            1 :     RUN_TEST(test_no_condition_matches_all);
     516            1 :     RUN_TEST(test_body_condition);
     517            1 :     RUN_TEST(test_age_condition);
     518            1 :     RUN_TEST(test_age_lt_condition);
     519            1 :     RUN_TEST(test_negation_if_not_from);
     520            1 :     RUN_TEST(test_negation_if_not_subject);
     521            1 :     RUN_TEST(test_negation_combined_with_positive);
     522            1 :     RUN_TEST(test_load_utf7_folder_name);
     523            1 :     RUN_TEST(test_rules_parse_file_features);
     524            1 :     RUN_TEST(test_rules_when_expression);
     525            1 :     RUN_TEST(test_rules_then_move_folder);
     526            1 :     RUN_TEST(test_rules_save_load_account);
     527            1 : }
        

Generated by: LCOV version 2.0-1