linux-kernel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Vincent Whitchurch <vincent.whitchurch@axis.com>
To: <linux-kernel@vger.kernel.org>
Cc: <kernel@axis.com>,
	Vincent Whitchurch <vincent.whitchurch@axis.com>,
	<devicetree@vger.kernel.org>, <linux-um@lists.infradead.org>,
	<shuah@kernel.org>, <brendanhiggins@google.com>,
	<linux-kselftest@vger.kernel.org>, <jic23@kernel.org>,
	<linux-iio@vger.kernel.org>, <lgirdwood@gmail.com>,
	<broonie@kernel.org>, <a.zummo@towertech.it>,
	<alexandre.belloni@bootlin.com>, <linux-rtc@vger.kernel.org>,
	<corbet@lwn.net>, <linux-doc@vger.kernel.org>
Subject: [RFC v1 10/10] rtc: pcf8563: add roadtest
Date: Fri, 11 Mar 2022 17:24:45 +0100	[thread overview]
Message-ID: <20220311162445.346685-11-vincent.whitchurch@axis.com> (raw)
In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com>

Add a roadtest for the PCF8563 RTC driver, testing many of the features
including alarm and invalid time handling.  Since it's the first
roadtest for RTC, some helper code for handling the ABI is included.

The following fixes were posted for problems identified during
development of these tests:

 - rtc: fix use-after-free on device removal
   https://lore.kernel.org/lkml/20211210160951.7718-1-vincent.whitchurch@axis.com/

 - rtc: pcf8563: clear RTC_FEATURE_ALARM if no irq
   https://lore.kernel.org/lkml/20220301131220.4011810-1-vincent.whitchurch@axis.com/

 - rtc: pcf8523: fix alarm interrupt disabling
   https://lore.kernel.org/lkml/20211103152253.22844-1-vincent.whitchurch@axis.com/
   (not the same hardware/driver, but this was the original target for
    test development)

Signed-off-by: Vincent Whitchurch <vincent.whitchurch@axis.com>
---
 .../roadtest/roadtest/tests/rtc/__init__.py   |   0
 .../roadtest/roadtest/tests/rtc/config        |   1 +
 .../roadtest/roadtest/tests/rtc/rtc.py        |  73 ++++
 .../roadtest/tests/rtc/test_pcf8563.py        | 348 ++++++++++++++++++
 4 files changed, 422 insertions(+)
 create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/__init__.py
 create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/config
 create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/rtc.py
 create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py

diff --git a/tools/testing/roadtest/roadtest/tests/rtc/__init__.py b/tools/testing/roadtest/roadtest/tests/rtc/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/tools/testing/roadtest/roadtest/tests/rtc/config b/tools/testing/roadtest/roadtest/tests/rtc/config
new file mode 100644
index 000000000000..f3654f9d7c19
--- /dev/null
+++ b/tools/testing/roadtest/roadtest/tests/rtc/config
@@ -0,0 +1 @@
+CONFIG_RTC_DRV_PCF8563=m
diff --git a/tools/testing/roadtest/roadtest/tests/rtc/rtc.py b/tools/testing/roadtest/roadtest/tests/rtc/rtc.py
new file mode 100644
index 000000000000..1a2855bfc195
--- /dev/null
+++ b/tools/testing/roadtest/roadtest/tests/rtc/rtc.py
@@ -0,0 +1,73 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright Axis Communications AB
+
+import contextlib
+import fcntl
+import struct
+import typing
+from pathlib import Path
+from typing import Any, cast
+
+RTC_RD_TIME = 0x80247009
+RTC_SET_TIME = 0x4024700A
+RTC_WKALM_SET = 0x4028700F
+RTC_VL_READ = 0x80047013
+
+RTC_IRQF = 0x80
+RTC_AF = 0x20
+
+RTC_VL_DATA_INVALID = 1 << 0
+
+
+class RTCTime(typing.NamedTuple):
+    tm_sec: int
+    tm_min: int
+    tm_hour: int
+    tm_mday: int
+    tm_mon: int
+    tm_year: int
+    tm_wday: int
+    tm_yday: int
+    tm_isdst: int
+
+
+class RTC(contextlib.AbstractContextManager):
+    def __init__(self, devpath: Path) -> None:
+        rtc = next(devpath.glob("rtc/rtc*")).name
+        self.filename = f"/dev/{rtc}"
+
+    def __enter__(self) -> "RTC":
+        self.file = open(self.filename, "rb")
+        return self
+
+    def __exit__(self, *_: Any) -> None:
+        self.file.close()
+
+    def read_time(self) -> RTCTime:
+        s = struct.Struct("9i")
+        buf = bytearray(s.size)
+        fcntl.ioctl(self.file.fileno(), RTC_RD_TIME, buf)
+        return RTCTime._make(s.unpack(buf))
+
+    def set_time(self, tm: RTCTime) -> int:
+        s = struct.Struct("9i")
+        buf = bytearray(s.size)
+        s.pack_into(buf, 0, *tm)
+        return fcntl.ioctl(self.file.fileno(), RTC_SET_TIME, buf)
+
+    def set_wake_alarm(self, enabled: bool, time: RTCTime) -> int:
+        s = struct.Struct("2B9i")
+        buf = bytearray(s.size)
+        s.pack_into(buf, 0, enabled, False, *time)
+        return fcntl.ioctl(self.file.fileno(), RTC_WKALM_SET, buf)
+
+    def read(self) -> int:
+        s = struct.Struct("L")
+        buf = self.file.read(s.size)
+        return cast(int, s.unpack(buf)[0])
+
+    def read_vl(self) -> int:
+        s = struct.Struct("I")
+        buf = bytearray(s.size)
+        fcntl.ioctl(self.file.fileno(), RTC_VL_READ, buf)
+        return cast(int, s.unpack(buf)[0])
diff --git a/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py b/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py
new file mode 100644
index 000000000000..a9f4c6d92762
--- /dev/null
+++ b/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py
@@ -0,0 +1,348 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# Copyright Axis Communications AB
+
+import errno
+import logging
+from typing import Any, Final, Optional
+
+from roadtest.backend.i2c import I2CModel
+from roadtest.core.devicetree import DtFragment, DtVar
+from roadtest.core.hardware import Hardware
+from roadtest.core.modules import insmod
+from roadtest.core.suite import UMLTestCase
+from roadtest.core.sysfs import I2CDriver
+
+from . import rtc
+
+logger = logging.getLogger(__name__)
+
+REG_CONTROL_STATUS_1: Final = 0x00
+REG_CONTROL_STATUS_2: Final = 0x01
+REG_VL_SECONDS: Final = 0x02
+REG_VL_MINUTES: Final = 0x03
+REG_VL_HOURS: Final = 0x04
+REG_VL_DAYS: Final = 0x05
+REG_VL_WEEKDAYS: Final = 0x06
+REG_VL_CENTURY_MONTHS: Final = 0x07
+REG_VL_YEARS: Final = 0x08
+REG_VL_MINUTE_ALARM: Final = 0x09
+REG_VL_HOUR_ALARM: Final = 0x0A
+REG_VL_DAY_ALARM: Final = 0x0B
+REG_VL_WEEKDAY_ALARM: Final = 0x0C
+REG_CLKOUT_CONTROL: Final = 0x0D
+REG_TIMER_CONTROL: Final = 0x0E
+REG_TIMER: Final = 0x0F
+
+REG_CONTROL_STATUS_2_AIE: Final = 1 << 1
+REG_CONTROL_STATUS_2_AF: Final = 1 << 3
+
+REG_VL_CENTURY_MONTHS_C: Final = 1 << 7
+
+REG_VL_ALARM_AE: Final = 1 << 7
+
+
+class PCF8563(I2CModel):
+    def __init__(self, int: Optional[int] = None, **kwargs: Any) -> None:
+        super().__init__(**kwargs)
+        self.int = int
+        self._set_int(False)
+
+        self.reg_addr = 0
+        # Reset values from Table 27 in datasheet, with X and - bits set to 0
+        self.regs = {
+            REG_CONTROL_STATUS_1: 0b_0000_1000,
+            REG_CONTROL_STATUS_2: 0b_0000_0000,
+            REG_VL_SECONDS: 0b_1000_0000,
+            REG_VL_MINUTES: 0b_0000_0000,
+            REG_VL_HOURS: 0b_0000_0000,
+            REG_VL_DAYS: 0b_0000_0000,
+            REG_VL_WEEKDAYS: 0b_0000_0000,
+            REG_VL_CENTURY_MONTHS: 0b_0000_0000,
+            REG_VL_YEARS: 0b_0000_0000,
+            REG_VL_MINUTE_ALARM: 0b_1000_0000,
+            REG_VL_HOUR_ALARM: 0b_1000_0000,
+            REG_VL_DAY_ALARM: 0b_1000_0000,
+            REG_VL_WEEKDAY_ALARM: 0b_1000_0000,
+            REG_CLKOUT_CONTROL: 0b_1000_0000,
+            REG_TIMER_CONTROL: 0b_0000_0011,
+            REG_TIMER: 0b_0000_0000,
+        }
+
+    def _set_int(self, active: int) -> None:
+        # Active-low
+        self.backend.gpio.set(self.int, not active)
+
+    def _check_alarm(self, addr: int) -> None:
+        alarmregs = [
+            REG_VL_MINUTE_ALARM,
+            REG_VL_HOUR_ALARM,
+            REG_VL_DAY_ALARM,
+            REG_VL_WEEKDAY_ALARM,
+        ]
+        timeregs = [
+            REG_VL_MINUTES,
+            REG_VL_HOURS,
+            REG_VL_DAYS,
+            REG_VL_WEEKDAYS,
+        ]
+
+        if addr not in alarmregs + timeregs:
+            return
+
+        af = all(
+            self.regs[a] == self.regs[b]
+            for a, b in zip(alarmregs, timeregs)
+            if not self.regs[a] & REG_VL_ALARM_AE
+        )
+        self.reg_write(REG_CONTROL_STATUS_2, self.regs[REG_CONTROL_STATUS_2] | af << 3)
+
+    def _update_irq(self) -> None:
+        aie = self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AIE
+        af = self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AF
+
+        logger.debug(f"{aie=} {af=}")
+        self._set_int(aie and af)
+
+    def reg_read(self, addr: int) -> int:
+        val = self.regs[addr]
+        return val
+
+    def reg_write(self, addr: int, val: int) -> None:
+        assert addr in self.regs
+        self.regs[addr] = val
+        logger.debug(f"{addr=:x} {val=:x}")
+        self._check_alarm(addr)
+        self._update_irq()
+
+    def read(self, len: int) -> bytes:
+        data = bytearray(len)
+
+        for i in range(len):
+            data[i] = self.reg_read(self.reg_addr)
+            self.reg_addr = self.reg_addr + 1
+
+        return bytes(data)
+
+    def write(self, data: bytes) -> None:
+        self.reg_addr = data[0]
+
+        for i, byte in enumerate(data[1:]):
+            addr = self.reg_addr + i
+            self.backend.mock.reg_write(addr, byte)
+            self.reg_write(addr, byte)
+
+
+class TestPCF8563(UMLTestCase):
+    dts = DtFragment(
+        src="""
+#include <dt-bindings/interrupt-controller/irq.h>
+
+&i2c {
+    rtc@$addr$ {
+        compatible = "nxp,pcf8563";
+        reg = <0x$addr$>;
+    };
+
+    rtc@$irqaddr$ {
+        compatible = "nxp,pcf8563";
+        reg = <0x$irqaddr$>;
+        interrupt-parent = <&gpio>;
+        interrupts = <$gpio$ IRQ_TYPE_LEVEL_LOW>;
+    };
+};
+        """,
+        variables={
+            "addr": DtVar.I2C_ADDR,
+            "irqaddr": DtVar.I2C_ADDR,
+            "gpio": DtVar.GPIO_PIN,
+        },
+    )
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        insmod("rtc-pcf8563")
+
+    @classmethod
+    def tearDownClass(cls) -> None:
+        # Can't rmmod since alarmtimer holds permanent reference
+        pass
+
+    def setUp(self) -> None:
+        self.driver = I2CDriver("rtc-pcf8563")
+        self.hw = Hardware("i2c")
+        self.hw.load_model(PCF8563, int=self.dts["gpio"])
+
+    def tearDown(self) -> None:
+        self.hw.close()
+
+    def test_read_time_invalid(self) -> None:
+        addr = self.dts["addr"]
+        with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev:
+            self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID)
+
+            with self.assertRaises(OSError) as cm:
+                rtcdev.read_time()
+            self.assertEqual(cm.exception.errno, errno.EINVAL)
+
+    def test_no_alarm_support(self) -> None:
+        addr = self.dts["addr"]
+        with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev:
+            # Make sure the times are valid so we don't get -EINVAL due to
+            # that.
+            tm = rtc.RTCTime(
+                tm_sec=10,
+                tm_min=1,
+                tm_hour=1,
+                tm_mday=1,
+                tm_mon=0,
+                tm_year=121,
+                tm_wday=0,
+                tm_yday=0,
+                tm_isdst=0,
+            )
+            rtcdev.set_time(tm)
+
+            alarmtm = tm._replace(tm_sec=0, tm_min=2)
+            with self.assertRaises(OSError) as cm:
+                rtcdev.set_wake_alarm(True, alarmtm)
+            self.assertEqual(cm.exception.errno, errno.EINVAL)
+
+    def test_alarm(self) -> None:
+        addr = self.dts["irqaddr"]
+        with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev:
+            tm = rtc.RTCTime(
+                tm_sec=10,
+                tm_min=1,
+                tm_hour=1,
+                tm_mday=1,
+                tm_mon=0,
+                tm_year=121,
+                tm_wday=5,
+                tm_yday=0,
+                tm_isdst=0,
+            )
+            rtcdev.set_time(tm)
+
+            alarmtm = tm._replace(tm_sec=0, tm_min=2)
+            rtcdev.set_wake_alarm(True, alarmtm)
+
+            mock = self.hw.update_mock()
+            mock.assert_last_reg_write(self, REG_VL_MINUTE_ALARM, 0x02)
+            mock.assert_last_reg_write(self, REG_VL_HOUR_ALARM, 0x01)
+            mock.assert_last_reg_write(self, REG_VL_DAY_ALARM, 0x01)
+            mock.assert_last_reg_write(self, REG_VL_WEEKDAY_ALARM, 5)
+            mock.assert_last_reg_write(
+                self, REG_CONTROL_STATUS_2, REG_CONTROL_STATUS_2_AIE
+            )
+            mock.reset_mock()
+
+            self.hw.reg_write(REG_VL_MINUTES, 0x02)
+            self.hw.kick()
+
+            # This waits for the interrupt
+            self.assertEqual(rtcdev.read() & 0xFF, rtc.RTC_IRQF | rtc.RTC_AF)
+
+            alarmtm = tm._replace(tm_sec=0, tm_min=3)
+            rtcdev.set_wake_alarm(False, alarmtm)
+
+            mock = self.hw.update_mock()
+            mock.assert_last_reg_write(self, REG_CONTROL_STATUS_2, 0)
+
+    def test_read_time_valid(self) -> None:
+        self.hw.reg_write(REG_VL_SECONDS, 0x37)
+        self.hw.reg_write(REG_VL_MINUTES, 0x10)
+        self.hw.reg_write(REG_VL_HOURS, 0x11)
+        self.hw.reg_write(REG_VL_DAYS, 0x25)
+        self.hw.reg_write(REG_VL_WEEKDAYS, 0x00)
+        self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12)
+        self.hw.reg_write(REG_VL_YEARS, 0x21)
+
+        addr = self.dts["addr"]
+        with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev:
+            tm = rtcdev.read_time()
+            self.assertEqual(
+                tm,
+                rtc.RTCTime(
+                    tm_sec=37,
+                    tm_min=10,
+                    tm_hour=11,
+                    tm_mday=25,
+                    tm_mon=11,
+                    tm_year=121,
+                    tm_wday=0,
+                    tm_yday=0,
+                    tm_isdst=0,
+                ),
+            )
+
+    def test_set_time_after_invalid(self) -> None:
+        addr = self.dts["addr"]
+        with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev:
+            self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID)
+
+            tm = rtc.RTCTime(
+                tm_sec=37,
+                tm_min=10,
+                tm_hour=11,
+                tm_mday=25,
+                tm_mon=11,
+                tm_year=121,
+                tm_wday=0,
+                tm_yday=0,
+                tm_isdst=0,
+            )
+
+            rtcdev.set_time(tm)
+            tm2 = rtcdev.read_time()
+            self.assertEqual(tm, tm2)
+
+            mock = self.hw.update_mock()
+            mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37)
+            mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10)
+            mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11)
+            mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25)
+            mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00)
+            # The driver uses the wrong polarity of the Century bit
+            # if the time was invalid.  This probably doesn't matter(?).
+            mock.assert_reg_write_once(self, REG_VL_CENTURY_MONTHS, 0 << 7 | 0x12)
+            mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21)
+
+            self.assertEqual(rtcdev.read_vl(), 0)
+
+    def test_set_time_after_valid(self) -> None:
+        self.hw.reg_write(REG_VL_SECONDS, 0x37)
+        self.hw.reg_write(REG_VL_MINUTES, 0x10)
+        self.hw.reg_write(REG_VL_HOURS, 0x11)
+        self.hw.reg_write(REG_VL_DAYS, 0x25)
+        self.hw.reg_write(REG_VL_WEEKDAYS, 0x00)
+        self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12)
+        self.hw.reg_write(REG_VL_YEARS, 0x21)
+
+        addr = self.dts["addr"]
+        with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev:
+            tm = rtc.RTCTime(
+                tm_sec=37,
+                tm_min=10,
+                tm_hour=11,
+                tm_mday=25,
+                tm_mon=11,
+                tm_year=121,
+                tm_wday=0,
+                tm_yday=0,
+                tm_isdst=0,
+            )
+
+            rtcdev.set_time(tm)
+            tm2 = rtcdev.read_time()
+            self.assertEqual(tm, tm2)
+
+            mock = self.hw.update_mock()
+            mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37)
+            mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10)
+            mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11)
+            mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25)
+            mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00)
+            mock.assert_reg_write_once(
+                self, REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12
+            )
+            mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21)
-- 
2.34.1


  parent reply	other threads:[~2022-03-11 16:28 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-03-11 16:24 [RFC v1 00/10] roadtest: a driver testing framework Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 01/10] roadtest: import libvhost-user from QEMU Vincent Whitchurch
2022-03-24 13:00   ` Johannes Berg
2022-04-05 13:54     ` Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 02/10] roadtest: add C backend Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 03/10] roadtest: add framework Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 04/10] roadtest: add base config Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 05/10] roadtest: add build files Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 06/10] roadtest: add documentation Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 07/10] iio: light: opt3001: add roadtest Vincent Whitchurch
2022-03-14 23:11   ` Brendan Higgins
2022-03-18 15:49     ` Vincent Whitchurch
2022-03-18 20:09       ` Johannes Berg
2022-03-29 14:43         ` Vincent Whitchurch
2022-03-29 14:50           ` Johannes Berg
2022-03-29 14:52             ` Johannes Berg
2022-03-11 16:24 ` [RFC v1 08/10] iio: light: vcnl4000: " Vincent Whitchurch
2022-03-20 17:02   ` Jonathan Cameron
2022-04-05 13:48     ` Vincent Whitchurch
2022-04-06 13:08       ` Jonathan Cameron
2022-04-14 10:20         ` Vincent Whitchurch
2022-03-11 16:24 ` [RFC v1 09/10] regulator: tps62864: " Vincent Whitchurch
2022-03-11 18:06   ` Mark Brown
2022-03-17 15:13     ` Vincent Whitchurch
2022-03-17 17:53       ` Mark Brown
2022-04-05 14:02         ` Vincent Whitchurch
2022-03-11 16:24 ` Vincent Whitchurch [this message]
2022-03-14 22:24 ` [RFC v1 00/10] roadtest: a driver testing framework Brendan Higgins
2022-03-17 16:09   ` Vincent Whitchurch
2022-04-18 19:44 ` Jonathan Cameron

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20220311162445.346685-11-vincent.whitchurch@axis.com \
    --to=vincent.whitchurch@axis.com \
    --cc=a.zummo@towertech.it \
    --cc=alexandre.belloni@bootlin.com \
    --cc=brendanhiggins@google.com \
    --cc=broonie@kernel.org \
    --cc=corbet@lwn.net \
    --cc=devicetree@vger.kernel.org \
    --cc=jic23@kernel.org \
    --cc=kernel@axis.com \
    --cc=lgirdwood@gmail.com \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-iio@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-rtc@vger.kernel.org \
    --cc=linux-um@lists.infradead.org \
    --cc=shuah@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).