--- /dev/null
+*** Settings ***
+Suite Setup Rspamadm Setup
+Suite Teardown Rspamadm Teardown
+Library ${RSPAMD_TESTDIR}/lib/rspamd.py
+Resource ${RSPAMD_TESTDIR}/lib/rspamd.robot
+Test Timeout 1 minute
+
+*** Variables ***
+${LOGSTATS_LOG} ${RSPAMD_TESTDIR}/data/logstats_test.data
+
+*** Test Cases ***
+Logstats JSON output
+ ${result} = Rspamadm logstats --json ${LOGSTATS_LOG}
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} "total"
+ Should Contain ${result.stdout} "no action"
+ Should Contain ${result.stdout} "reject"
+ Should Contain ${result.stdout} "add header"
+ Should Contain ${result.stdout} "greylist"
+ Should Contain ${result.stdout} "symbols"
+
+Logstats text output
+ ${result} = Rspamadm logstats ${LOGSTATS_LOG}
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} Messages scanned: 10
+ Should Contain ${result.stdout} === Summary
+ Should Contain ${result.stdout} no action
+ Should Contain ${result.stdout} reject
+ Should Contain ${result.stdout} add header
+ Should Contain ${result.stdout} greylist
+
+Logstats symbol filter
+ ${result} = Rspamadm logstats -s BAYES_SPAM ${LOGSTATS_LOG}
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} BAYES_SPAM
+ Should Not Contain ${result.stdout} R_SPF_ALLOW
+ Should Contain ${result.stdout} Messages scanned: 10
+
+Logstats alpha score warning
+ ${result} = Rspamadm logstats ${LOGSTATS_LOG}
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} WARNING:
+ Should Contain ${result.stdout} DKIM_TRACE
+ Should Contain ${result.stdout} alpha_score
+
+Logstats stdin
+ ${result} = Run Process ${RSPAMADM}
+ ... --var\=TMPDIR\=${RSPAMADM_TMPDIR}
+ ... --var\=DBDIR\=${RSPAMADM_TMPDIR}
+ ... --var\=LOCAL_CONFDIR\=/nonexistent
+ ... logstats --json -
+ ... stdin=${LOGSTATS_LOG}
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} "total"
+ Should Contain ${result.stdout} "no action"
+
+Logstats scan time
+ ${result} = Rspamadm logstats ${LOGSTATS_LOG}
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} scan time min/avg/max
--- /dev/null
+*** Settings ***
+Suite Setup Rspamadm Setup
+Suite Teardown Rspamadm Teardown
+Library ${RSPAMD_TESTDIR}/lib/rspamd.py
+Resource ${RSPAMD_TESTDIR}/lib/rspamd.robot
+Test Timeout 1 minute
+
+*** Variables ***
+${MAPSTATS_LOG} ${RSPAMD_TESTDIR}/data/mapstats_test.data
+${MAPSTATS_CONF} ${RSPAMD_TESTDIR}/configs/mapstats_test.conf
+
+*** Test Cases ***
+Mapstats map loading
+ ${result} = Run Mapstats
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} TEST_PLAIN
+ Should Contain ${result.stdout} TEST_IP
+ Should Contain ${result.stdout} TEST_RE
+ Should Contain ${result.stdout} [OK]
+
+Mapstats plain map comments
+ ${result} = Run Mapstats
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} \# Test domain
+ Should Contain ${result.stdout} \# Scored entry
+
+Mapstats IP map comments
+ ${result} = Run Mapstats
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} \# Private network
+ Should Contain ${result.stdout} \# Internal
+
+Mapstats regexp map comments
+ ${result} = Run Mapstats
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} \# Test pattern
+
+Mapstats plain match counts
+ ${result} = Run Mapstats
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} example.com
+ Should Contain ${result.stdout} test.org
+
+Mapstats regexp match
+ ${result} = Run Mapstats
+ Should Be Equal As Integers ${result.rc} 0
+ Should Contain ${result.stdout} test-string-
+
+*** Keywords ***
+Run Mapstats
+ ${result} = Rspamadm --var\=TESTDIR\=${RSPAMD_TESTDIR} mapstats -c ${MAPSTATS_CONF} ${MAPSTATS_LOG}
+ RETURN ${result}
--- /dev/null
+multimap {
+ TEST_PLAIN {
+ type = "from";
+ map = "{= env.TESTDIR =}/data/mapstats_plain.map";
+ }
+ TEST_IP {
+ type = "ip";
+ map = "{= env.TESTDIR =}/data/mapstats_ip.map";
+ }
+ TEST_RE {
+ type = "from";
+ regexp = true;
+ map = "{= env.TESTDIR =}/data/mapstats_re.map";
+ }
+}
--- /dev/null
+2026-02-01 10:00:01.100 #1001(normal) <a1b2c3>; rspamd_task_write_log: id: <msg001@test.com>, qid: <Q001>, ip: 10.0.0.1, (no action): [1.5/15.0] [R_SPF_ALLOW(-1.0),BAYES_HAM(-3.0),MIME_GOOD(-0.1),DKIM_TRACE(0.0)], len: 1234, time: 5.20ms
+2026-02-01 10:00:02.200 #1001(normal) <d4e5f6>; rspamd_task_write_log: id: <msg002@test.com>, qid: <Q002>, ip: 10.0.0.2, (reject): [18.5/15.0] [BAYES_SPAM(5.0),RBL_SPAMHAUS(4.0),SURBL_MULTI(5.5),FORGED_SENDER(3.0),MISSING_MID(1.0)], len: 2345, time: 15.30ms
+2026-02-01 10:00:03.300 #1001(normal) <g7h8i9>; rspamd_task_write_log: id: <msg003@test.com>, qid: <Q003>, ip: 10.0.0.3, (add header): [8.2/15.0] [BAYES_SPAM(5.0),RBL_SPAMHAUS(4.0),R_SPF_SOFTFAIL(1.0),MISSING_MID(1.0)], len: 3456, time: 12.10ms
+2026-02-01 10:00:04.400 #1001(normal) <j0k1l2>; rspamd_task_write_log: id: <msg004@test.com>, qid: <Q004>, ip: 10.0.0.4, (no action): [0.5/15.0] [R_SPF_ALLOW(-1.0),R_DKIM_ALLOW(-0.5),MIME_GOOD(-0.1),DKIM_TRACE(0.0)], len: 4567, time: 3.80ms
+2026-02-01 10:00:05.500 #1001(normal) <m3n4o5>; rspamd_task_write_log: id: <msg005@test.com>, qid: <Q005>, ip: 10.0.0.5, (reject): [22.0/15.0] [BAYES_SPAM(5.0),RBL_SPAMHAUS(4.0),SURBL_MULTI(5.5),FORGED_SENDER(3.0),MISSING_MID(1.0),GTUBE(3.5)], len: 5678, time: 20.50ms
+2026-02-01 10:00:06.600 #1001(normal) <p6q7r8>; rspamd_task_write_log: id: <msg006@test.com>, qid: <Q006>, ip: 10.0.0.6, (greylist): [4.5/15.0] [BAYES_SPAM(5.0),R_SPF_ALLOW(-1.0),MIME_GOOD(-0.1)], len: 6789, time: 8.40ms
+2026-02-01 10:00:07.700 #1001(normal) <s9t0u1>; rspamd_task_write_log: id: <msg007@test.com>, qid: <Q007>, ip: 10.0.0.7, (no action): [-1.0/15.0] [R_SPF_ALLOW(-1.0),R_DKIM_ALLOW(-0.5),BAYES_HAM(-3.0),MIME_GOOD(-0.1),DKIM_TRACE(0.0)], len: 7890, time: 4.60ms
+2026-02-01 10:00:08.800 #1001(normal) <v2w3x4>; rspamd_task_write_log: id: <msg008@test.com>, qid: <Q008>, ip: 10.0.0.8, (add header): [9.1/15.0] [BAYES_SPAM(5.0),SURBL_MULTI(5.5),R_SPF_SOFTFAIL(1.0)], len: 8901, time: 18.20ms
+2026-02-01 10:00:09.900 #1001(normal) <y5z6a7>; rspamd_task_write_log: id: <msg009@test.com>, qid: <Q009>, ip: 10.0.0.9, (no action): [2.0/15.0] [FORGED_SENDER(3.0),R_SPF_ALLOW(-1.0)], len: 9012, time: 6.30ms
+2026-02-01 10:00:10.100 #1001(normal) <b8c9d0>; rspamd_task_write_log: id: <msg010@test.com>, qid: <Q010>, ip: 10.0.0.10, (reject): [16.0/15.0] [BAYES_SPAM(5.0),RBL_SPAMHAUS(4.0),FORGED_SENDER(3.0),SURBL_MULTI(5.5),MISSING_MID(1.0)], len: 1023, time: 14.70ms
--- /dev/null
+192.168.1.0/24 # Private network
+10.0.0.0/8 # Internal
+# Comment line
+172.16.0.0/12 # Other private
--- /dev/null
+example.com # Test domain
+test.org 1.5 # Scored entry
+foo.bar
+# This is a comment line
+unmatched.example # Won't be hit
--- /dev/null
+/test-string-\d+/i # Test pattern
+/^foobar$/ # Exact match
+# Comment line
+/unused-pattern/ # Won't match
--- /dev/null
+2026-02-01 10:00:01.100 #1001(normal) <a1b2c3>; rspamd_task_write_log: id: <msg001@test.com>, qid: <Q001>, ip: 10.0.0.1, (no action): [2.0/15.0] [TEST_PLAIN(1.0){example.com;},R_SPF_ALLOW(-1.0)], len: 1000, time: 5.00ms
+2026-02-01 10:00:02.200 #1001(normal) <d4e5f6>; rspamd_task_write_log: id: <msg002@test.com>, qid: <Q002>, ip: 192.168.1.100, (no action): [3.0/15.0] [TEST_PLAIN(1.0){test.org;},TEST_IP(2.0){192.168.1.100;}], len: 2000, time: 8.00ms
+2026-02-01 10:00:03.300 #1001(normal) <g7h8i9>; rspamd_task_write_log: id: <msg003@test.com>, qid: <Q003>, ip: 10.0.0.3, (no action): [1.5/15.0] [TEST_RE(1.5){test-string-123;}], len: 3000, time: 6.00ms
+2026-02-01 10:00:04.400 #1001(normal) <j0k1l2>; rspamd_task_write_log: id: <msg004@test.com>, qid: <Q004>, ip: 10.0.0.4, (no action): [1.0/15.0] [TEST_PLAIN(1.0){example.com;}], len: 4000, time: 4.00ms
+2026-02-01 10:00:05.500 #1001(normal) <m3n4o5>; rspamd_task_write_log: id: <msg005@test.com>, qid: <Q005>, ip: 10.5.3.1, (no action): [2.0/15.0] [TEST_IP(2.0){10.5.3.1;}], len: 5000, time: 7.00ms