Module Name:    src
Committed By:   thorpej
Date:           Wed Oct 20 03:13:14 UTC 2021

Modified Files:
        src/sys/kern: vnode_if.c
        src/sys/rump/include/rump: rumpvnode_if.h
        src/sys/rump/librump/rumpvfs: rumpvnode_if.c
        src/sys/sys: vnode_if.h

Log Message:
Regen for:

Overhaul of the EVFILT_VNODE kevent(2) filter:

- Centralize vnode kevent handling in the VOP_*() wrappers, rather than
  forcing each individual file system to deal with it (except VOP_RENAME(),
  because VOP_RENAME() is a mess and we currently have 2 different ways
  of handling it; at least it's reasonably well-centralized in the "new"
  way).
- Add support for NOTE_OPEN, NOTE_CLOSE, NOTE_CLOSE_WRITE, and NOTE_READ,
  compatible with the same events in FreeBSD.
- Track which kevent notifications clients are interested in receiving
  to avoid doing work for events no one cares about (avoiding, e.g.
  taking locks and traversing the klist to send a NOTE_WRITE when
  someone is merely watching for a file to be deleted, for example).

In support of the above:

- Add support in vnode_if.sh for specifying PRE- and POST-op handlers,
  to be invoked before and after vop_pre() and vop_post(), respectively.
  Basic idea from FreeBSD, but implemented differently.
- Add support in vnode_if.sh for specifying CONTEXT fields in the
  vop_*_args structures.  These context fields are used to convey information
  between the file system VOP function and the VOP wrapper, but do not
  occupy an argument slot in the VOP_*() call itself.  These context fields
  are initialized and subsequently interpreted by PRE- and POST-op handlers.
- Version VOP_REMOVE(), uses the a context field for the file system to report
  back the resulting link count of the target vnode.  Return this in tmpfs,
  udf, nfs, chfs, ext2fs, lfs, and ufs.

NetBSD 9.99.92.


To generate a diff of this commit:
cvs rdiff -u -r1.114 -r1.115 src/sys/kern/vnode_if.c
cvs rdiff -u -r1.36 -r1.37 src/sys/rump/include/rump/rumpvnode_if.h
cvs rdiff -u -r1.36 -r1.37 src/sys/rump/librump/rumpvfs/rumpvnode_if.c
cvs rdiff -u -r1.107 -r1.108 src/sys/sys/vnode_if.h

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

Modified files:

Index: src/sys/kern/vnode_if.c
diff -u src/sys/kern/vnode_if.c:1.114 src/sys/kern/vnode_if.c:1.115
--- src/sys/kern/vnode_if.c:1.114	Fri Jul  2 16:57:15 2021
+++ src/sys/kern/vnode_if.c	Wed Oct 20 03:13:14 2021
@@ -1,13 +1,13 @@
-/*	$NetBSD: vnode_if.c,v 1.114 2021/07/02 16:57:15 dholland Exp $	*/
+/*	$NetBSD: vnode_if.c,v 1.115 2021/10/20 03:13:14 thorpej Exp $	*/
 
 /*
  * Warning: DO NOT EDIT! This file is automatically generated!
  * (Modifications made here may easily be lost!)
  *
  * Created from the file:
- *	NetBSD: vnode_if.src,v 1.82 2021/07/02 16:56:22 dholland Exp
+ *	NetBSD: vnode_if.src,v 1.83 2021/10/20 03:08:18 thorpej Exp
  * by the script:
- *	NetBSD: vnode_if.sh,v 1.70 2020/05/16 18:31:50 christos Exp
+ *	NetBSD: vnode_if.sh,v 1.72 2021/10/20 03:08:18 thorpej Exp
  */
 
 /*
@@ -40,11 +40,12 @@
  */
 
 #include <sys/cdefs.h>
-__KERNEL_RCSID(0, "$NetBSD: vnode_if.c,v 1.114 2021/07/02 16:57:15 dholland Exp $");
+__KERNEL_RCSID(0, "$NetBSD: vnode_if.c,v 1.115 2021/10/20 03:13:14 thorpej Exp $");
 
 #include <sys/param.h>
 #include <sys/mount.h>
 #include <sys/buf.h>
+#include <sys/fcntl.h>
 #include <sys/vnode.h>
 #include <sys/lock.h>
 #include <sys/fstrans.h>
@@ -89,6 +90,199 @@ vop_pre(vnode_t *vp, struct mount **mp, 
 	return 0;
 }
 
+static inline u_quad_t
+vop_pre_get_size(struct vnode *vp)
+{
+	mutex_enter(vp->v_interlock);
+	KASSERT(vp->v_size != VSIZENOTSET);
+	u_quad_t rv = (u_quad_t)vp->v_size;
+	mutex_exit(vp->v_interlock);
+
+	return rv;
+}
+
+/*
+ * VOP_RMDIR(), VOP_REMOVE(), and VOP_RENAME() need special handling
+ * because they each drop the caller's references on one or more of
+ * their arguments.  While there must be an open file descriptor in
+ * associated with a vnode in order for knotes to be attached to it,
+ * that status could change during the course of the operation.  So,
+ * for the vnode arguments that are WILLRELE or WILLPUT, we check
+ * pre-op if there are registered knotes, take a hold count if so,
+ * and post-op release the hold after activating any knotes still
+ * associated with the vnode.
+ */
+
+#define	VOP_POST_KNOTE(thisvp, e, n)					\
+do {									\
+	if (__predict_true((e) == 0)) {					\
+		/*							\
+		 * VN_KNOTE() does the VN_KEVENT_INTEREST()		\
+		 * check for us.					\
+		 */							\
+		VN_KNOTE((thisvp), (n));				\
+	}								\
+} while (/*CONSTCOND*/0)
+
+#define	VOP_POST_KNOTE_HELD(thisvp, e, n)				\
+do {									\
+	/*								\
+	 * We don't perform a VN_KEVENT_INTEREST() check here; it	\
+	 * was already performed when we did the pre-op work that	\
+	 * caused the vnode to be held in the first place.		\
+	 */								\
+	mutex_enter((thisvp)->v_interlock);				\
+	if (__predict_true((e) == 0)) {					\
+		knote(&(thisvp)->v_klist, (n));				\
+	}								\
+	holdrelel((thisvp));						\
+	mutex_exit((thisvp)->v_interlock);				\
+	/*								\
+	 * thisvp might be gone now!  Don't touch!			\
+	 */								\
+} while (/*CONSTCOND*/0)
+
+#define	vop_create_post(ap, e)						\
+	VOP_POST_KNOTE((ap)->a_dvp, (e), NOTE_WRITE)
+
+#define	vop_mknod_post(ap, e)						\
+	VOP_POST_KNOTE((ap)->a_dvp, (e), NOTE_WRITE)
+
+#define	vop_setattr_pre(ap)						\
+	u_quad_t osize = 0;						\
+	long vp_events =						\
+	    VN_KEVENT_INTEREST((ap)->a_vp, NOTE_ATTRIB | NOTE_EXTEND)	\
+	    ? NOTE_ATTRIB : 0;						\
+	bool check_extend = false;					\
+	if (__predict_false(vp_events != 0 &&				\
+	    (ap)->a_vap->va_size != VNOVALSIZE)) {			\
+		check_extend = true;					\
+		osize = vop_pre_get_size((ap)->a_vp);			\
+	}
+
+#define	vop_setattr_post(ap, e)						\
+do {									\
+	if (__predict_false(vp_events != 0)) {				\
+		if (__predict_false(check_extend &&			\
+		    (ap)->a_vap->va_size > osize)) {			\
+			vp_events |= NOTE_EXTEND;			\
+		}							\
+		VOP_POST_KNOTE((ap)->a_vp, (e), vp_events);		\
+	}								\
+} while (/*CONSTCOND*/0)
+
+#define	vop_setacl_post(ap, e)						\
+	VOP_POST_KNOTE((ap)->a_vp, (e), NOTE_ATTRIB)
+
+#define	vop_link_post(ap, e)						\
+do {									\
+	VOP_POST_KNOTE((ap)->a_dvp, (e), NOTE_WRITE);			\
+	VOP_POST_KNOTE((ap)->a_vp, (e), NOTE_LINK);			\
+} while (/*CONSTCOND*/0)
+
+#define	vop_mkdir_post(ap, e)						\
+	VOP_POST_KNOTE((ap)->a_dvp, (e), NOTE_WRITE | NOTE_LINK)
+
+#define	vop_remove_pre_common(ap)					\
+	bool post_event_vp =						\
+	    VN_KEVENT_INTEREST((ap)->a_vp, NOTE_DELETE | NOTE_LINK);	\
+	if (__predict_false(post_event_vp)) {				\
+		vhold((ap)->a_vp);					\
+	}
+
+#define	vop_remove_post_common(ap, e, dn, lc)				\
+do {									\
+	VOP_POST_KNOTE((ap)->a_dvp, (e), (dn));				\
+	if (__predict_false(post_event_vp)) {				\
+		VOP_POST_KNOTE_HELD((ap)->a_vp, (e),			\
+		    (lc) ? NOTE_LINK : NOTE_DELETE);			\
+	}								\
+} while (/*CONSTCOND*/0)
+
+/*
+ * One could make the argument that VOP_REMOVE() should send NOTE_LINK
+ * on vp if the resulting link count is not zero, but that's not what
+ * the documentation says.
+ *
+ * We could change this easily by passing ap->ctx_vp_new_nlink to
+ * vop_remove_post_common().
+ */
+#define	vop_remove_pre(ap)						\
+	vop_remove_pre_common((ap));					\
+	/*								\
+	 * We will assume that the file being removed is deleted unless	\
+	 * the file system tells us otherwise by updating vp_new_nlink.	\
+	 */								\
+	(ap)->ctx_vp_new_nlink = 0;
+
+#define	vop_remove_post(ap, e)						\
+	vop_remove_post_common((ap), (e), NOTE_WRITE, 0)
+
+#define	vop_rmdir_pre(ap)						\
+	vop_remove_pre_common(ap)
+
+#define	vop_rmdir_post(ap, e)						\
+	vop_remove_post_common((ap), (e), NOTE_WRITE | NOTE_LINK, 0)
+
+#define	vop_symlink_post(ap, e)						\
+	VOP_POST_KNOTE((ap)->a_dvp, (e), NOTE_WRITE)
+
+#define	vop_open_post(ap, e)						\
+	VOP_POST_KNOTE((ap)->a_vp, (e), NOTE_OPEN)
+
+#define	vop_close_post(ap, e)						\
+do {									\
+	extern int (**dead_vnodeop_p)(void *);				\
+									\
+	/* See the definition of VN_KNOTE() in <sys/vnode.h>. */	\
+	if (__predict_false(VN_KEVENT_INTEREST((ap)->a_vp,		\
+	    NOTE_CLOSE_WRITE | NOTE_CLOSE) && (e) == 0)) {		\
+		struct vnode *thisvp = (ap)->a_vp;			\
+		mutex_enter(thisvp->v_interlock);			\
+		/*							\
+		 * Don't send NOTE_CLOSE when closing a vnode that's	\
+		 * been reclaimed or otherwise revoked; a NOTE_REVOKE	\
+		 * has already been sent, and this close is effectively	\
+		 * meaningless from the watcher's perspective.		\
+		 */							\
+		if (__predict_true(thisvp->v_op != dead_vnodeop_p)) {	\
+			knote(&thisvp->v_klist,				\
+			    ((ap)->a_fflag & FWRITE)			\
+			    ? NOTE_CLOSE_WRITE : NOTE_CLOSE);		\
+		}							\
+		mutex_exit(thisvp->v_interlock);			\
+	}								\
+} while (/*CONSTCOND*/0)
+
+#define	vop_read_post(ap, e)						\
+	VOP_POST_KNOTE((ap)->a_vp, (e), NOTE_READ)
+
+#define	vop_write_pre(ap)						\
+	off_t ooffset = 0, noffset = 0;					\
+	u_quad_t osize = 0;						\
+	long vp_events =						\
+	    VN_KEVENT_INTEREST((ap)->a_vp, NOTE_WRITE | NOTE_EXTEND)	\
+	    ? NOTE_WRITE : 0;						\
+	if (__predict_false(vp_events != 0)) {				\
+		ooffset = (ap)->a_uio->uio_offset;			\
+		osize = vop_pre_get_size((ap)->a_vp);			\
+	}
+
+#define	vop_write_post(ap, e)						\
+do {									\
+	/*								\
+	 * If any data was written, we'll post an event, even if	\
+	 * there was an error.						\
+	 */								\
+	noffset = (ap)->a_uio->uio_offset;				\
+	if (__predict_false(vp_events != 0 && noffset > ooffset)) {	\
+		if (noffset > osize) {					\
+			vp_events |= NOTE_EXTEND;			\
+		}							\
+		VN_KNOTE((ap)->a_vp, vp_events);			\
+	}								\
+} while (/*CONSTCOND*/0)
+
 static inline void
 vop_post(vnode_t *vp, struct mount *mp, bool mpsafe, enum fst_op op)
 {
@@ -251,6 +445,7 @@ VOP_CREATE(struct vnode *dvp,
 		return error;
 	error = (VCALL(dvp, VOFFSET(vop_create), &a));
 	vop_post(dvp, mp, mpsafe, FST_NO);
+	vop_create_post(&a, error);
 #ifdef DIAGNOSTIC
 	if (error == 0)
 		KASSERT((*vpp)->v_size != VSIZENOTSET
@@ -292,6 +487,7 @@ VOP_MKNOD(struct vnode *dvp,
 		return error;
 	error = (VCALL(dvp, VOFFSET(vop_mknod), &a));
 	vop_post(dvp, mp, mpsafe, FST_NO);
+	vop_mknod_post(&a, error);
 #ifdef DIAGNOSTIC
 	if (error == 0)
 		KASSERT((*vpp)->v_size != VSIZENOTSET
@@ -331,6 +527,7 @@ VOP_OPEN(struct vnode *vp,
 		return error;
 	error = (VCALL(vp, VOFFSET(vop_open), &a));
 	vop_post(vp, mp, mpsafe, FST_NO);
+	vop_open_post(&a, error);
 	return error;
 }
 
@@ -365,6 +562,7 @@ VOP_CLOSE(struct vnode *vp,
 		return error;
 	error = (VCALL(vp, VOFFSET(vop_close), &a));
 	vop_post(vp, mp, mpsafe, FST_NO);
+	vop_close_post(&a, error);
 	return error;
 }
 
@@ -496,11 +694,13 @@ VOP_SETATTR(struct vnode *vp,
 	a.a_vp = vp;
 	a.a_vap = vap;
 	a.a_cred = cred;
+	vop_setattr_pre(&a);
 	error = vop_pre(vp, &mp, &mpsafe, FST_NO);
 	if (error)
 		return error;
 	error = (VCALL(vp, VOFFSET(vop_setattr), &a));
 	vop_post(vp, mp, mpsafe, FST_NO);
+	vop_setattr_post(&a, error);
 	return error;
 }
 
@@ -537,6 +737,7 @@ VOP_READ(struct vnode *vp,
 		return error;
 	error = (VCALL(vp, VOFFSET(vop_read), &a));
 	vop_post(vp, mp, mpsafe, FST_NO);
+	vop_read_post(&a, error);
 	return error;
 }
 
@@ -568,11 +769,13 @@ VOP_WRITE(struct vnode *vp,
 	a.a_uio = uio;
 	a.a_ioflag = ioflag;
 	a.a_cred = cred;
+	vop_write_pre(&a);
 	error = vop_pre(vp, &mp, &mpsafe, FST_NO);
 	if (error)
 		return error;
 	error = (VCALL(vp, VOFFSET(vop_write), &a));
 	vop_post(vp, mp, mpsafe, FST_NO);
+	vop_write_post(&a, error);
 	return error;
 }
 
@@ -925,8 +1128,8 @@ VOP_SEEK(struct vnode *vp,
 }
 
 const int vop_remove_vp_offsets[] = {
-	VOPARG_OFFSETOF(struct vop_remove_v2_args,a_dvp),
-	VOPARG_OFFSETOF(struct vop_remove_v2_args,a_vp),
+	VOPARG_OFFSETOF(struct vop_remove_v3_args,a_dvp),
+	VOPARG_OFFSETOF(struct vop_remove_v3_args,a_vp),
 	VDESC_NO_OFFSET
 };
 const struct vnodeop_desc vop_remove_desc = {
@@ -936,7 +1139,7 @@ const struct vnodeop_desc vop_remove_des
 	vop_remove_vp_offsets,
 	VDESC_NO_OFFSET,
 	VDESC_NO_OFFSET,
-	VOPARG_OFFSETOF(struct vop_remove_v2_args, a_cnp),
+	VOPARG_OFFSETOF(struct vop_remove_v3_args, a_cnp),
 };
 int
 VOP_REMOVE(struct vnode *dvp,
@@ -945,17 +1148,19 @@ VOP_REMOVE(struct vnode *dvp,
 {
 	int error;
 	bool mpsafe;
-	struct vop_remove_v2_args a;
+	struct vop_remove_v3_args a;
 	struct mount *mp;
 	a.a_desc = VDESC(vop_remove);
 	a.a_dvp = dvp;
 	a.a_vp = vp;
 	a.a_cnp = cnp;
+	vop_remove_pre(&a);
 	error = vop_pre(dvp, &mp, &mpsafe, FST_NO);
 	if (error)
 		return error;
 	error = (VCALL(dvp, VOFFSET(vop_remove), &a));
 	vop_post(dvp, mp, mpsafe, FST_NO);
+	vop_remove_post(&a, error);
 	return error;
 }
 
@@ -991,6 +1196,7 @@ VOP_LINK(struct vnode *dvp,
 		return error;
 	error = (VCALL(dvp, VOFFSET(vop_link), &a));
 	vop_post(dvp, mp, mpsafe, FST_NO);
+	vop_link_post(&a, error);
 	return error;
 }
 
@@ -1070,6 +1276,7 @@ VOP_MKDIR(struct vnode *dvp,
 		return error;
 	error = (VCALL(dvp, VOFFSET(vop_mkdir), &a));
 	vop_post(dvp, mp, mpsafe, FST_NO);
+	vop_mkdir_post(&a, error);
 #ifdef DIAGNOSTIC
 	if (error == 0)
 		KASSERT((*vpp)->v_size != VSIZENOTSET
@@ -1105,11 +1312,13 @@ VOP_RMDIR(struct vnode *dvp,
 	a.a_dvp = dvp;
 	a.a_vp = vp;
 	a.a_cnp = cnp;
+	vop_rmdir_pre(&a);
 	error = vop_pre(dvp, &mp, &mpsafe, FST_NO);
 	if (error)
 		return error;
 	error = (VCALL(dvp, VOFFSET(vop_rmdir), &a));
 	vop_post(dvp, mp, mpsafe, FST_NO);
+	vop_rmdir_post(&a, error);
 	return error;
 }
 
@@ -1148,6 +1357,7 @@ VOP_SYMLINK(struct vnode *dvp,
 		return error;
 	error = (VCALL(dvp, VOFFSET(vop_symlink), &a));
 	vop_post(dvp, mp, mpsafe, FST_NO);
+	vop_symlink_post(&a, error);
 #ifdef DIAGNOSTIC
 	if (error == 0)
 		KASSERT((*vpp)->v_size != VSIZENOTSET
@@ -1771,6 +1981,7 @@ VOP_SETACL(struct vnode *vp,
 		return error;
 	error = (VCALL(vp, VOFFSET(vop_setacl), &a));
 	vop_post(vp, mp, mpsafe, FST_YES);
+	vop_setacl_post(&a, error);
 	return error;
 }
 

Index: src/sys/rump/include/rump/rumpvnode_if.h
diff -u src/sys/rump/include/rump/rumpvnode_if.h:1.36 src/sys/rump/include/rump/rumpvnode_if.h:1.37
--- src/sys/rump/include/rump/rumpvnode_if.h:1.36	Fri Jul  2 16:57:15 2021
+++ src/sys/rump/include/rump/rumpvnode_if.h	Wed Oct 20 03:13:14 2021
@@ -1,13 +1,13 @@
-/*	$NetBSD: rumpvnode_if.h,v 1.36 2021/07/02 16:57:15 dholland Exp $	*/
+/*	$NetBSD: rumpvnode_if.h,v 1.37 2021/10/20 03:13:14 thorpej Exp $	*/
 
 /*
  * Warning: DO NOT EDIT! This file is automatically generated!
  * (Modifications made here may easily be lost!)
  *
  * Created from the file:
- *	NetBSD: vnode_if.src,v 1.82 2021/07/02 16:56:22 dholland Exp
+ *	NetBSD: vnode_if.src,v 1.83 2021/10/20 03:08:18 thorpej Exp
  * by the script:
- *	NetBSD: vnode_if.sh,v 1.70 2020/05/16 18:31:50 christos Exp
+ *	NetBSD: vnode_if.sh,v 1.72 2021/10/20 03:08:18 thorpej Exp
  */
 
 /*

Index: src/sys/rump/librump/rumpvfs/rumpvnode_if.c
diff -u src/sys/rump/librump/rumpvfs/rumpvnode_if.c:1.36 src/sys/rump/librump/rumpvfs/rumpvnode_if.c:1.37
--- src/sys/rump/librump/rumpvfs/rumpvnode_if.c:1.36	Fri Jul  2 16:57:16 2021
+++ src/sys/rump/librump/rumpvfs/rumpvnode_if.c	Wed Oct 20 03:13:14 2021
@@ -1,13 +1,13 @@
-/*	$NetBSD: rumpvnode_if.c,v 1.36 2021/07/02 16:57:16 dholland Exp $	*/
+/*	$NetBSD: rumpvnode_if.c,v 1.37 2021/10/20 03:13:14 thorpej Exp $	*/
 
 /*
  * Warning: DO NOT EDIT! This file is automatically generated!
  * (Modifications made here may easily be lost!)
  *
  * Created from the file:
- *	NetBSD: vnode_if.src,v 1.82 2021/07/02 16:56:22 dholland Exp
+ *	NetBSD: vnode_if.src,v 1.83 2021/10/20 03:08:18 thorpej Exp
  * by the script:
- *	NetBSD: vnode_if.sh,v 1.70 2020/05/16 18:31:50 christos Exp
+ *	NetBSD: vnode_if.sh,v 1.72 2021/10/20 03:08:18 thorpej Exp
  */
 
 /*
@@ -40,11 +40,12 @@
  */
 
 #include <sys/cdefs.h>
-__KERNEL_RCSID(0, "$NetBSD: rumpvnode_if.c,v 1.36 2021/07/02 16:57:16 dholland Exp $");
+__KERNEL_RCSID(0, "$NetBSD: rumpvnode_if.c,v 1.37 2021/10/20 03:13:14 thorpej Exp $");
 
 #include <sys/param.h>
 #include <sys/mount.h>
 #include <sys/buf.h>
+#include <sys/fcntl.h>
 #include <sys/vnode.h>
 #include <sys/lock.h>
 #include <rump/rumpvnode_if.h>

Index: src/sys/sys/vnode_if.h
diff -u src/sys/sys/vnode_if.h:1.107 src/sys/sys/vnode_if.h:1.108
--- src/sys/sys/vnode_if.h:1.107	Fri Jul  2 16:57:15 2021
+++ src/sys/sys/vnode_if.h	Wed Oct 20 03:13:14 2021
@@ -1,13 +1,13 @@
-/*	$NetBSD: vnode_if.h,v 1.107 2021/07/02 16:57:15 dholland Exp $	*/
+/*	$NetBSD: vnode_if.h,v 1.108 2021/10/20 03:13:14 thorpej Exp $	*/
 
 /*
  * Warning: DO NOT EDIT! This file is automatically generated!
  * (Modifications made here may easily be lost!)
  *
  * Created from the file:
- *	NetBSD: vnode_if.src,v 1.82 2021/07/02 16:56:22 dholland Exp
+ *	NetBSD: vnode_if.src,v 1.83 2021/10/20 03:08:18 thorpej Exp
  * by the script:
- *	NetBSD: vnode_if.sh,v 1.70 2020/05/16 18:31:50 christos Exp
+ *	NetBSD: vnode_if.sh,v 1.72 2021/10/20 03:08:18 thorpej Exp
  */
 
 /*
@@ -290,11 +290,12 @@ extern const struct vnodeop_desc vop_see
 int VOP_SEEK(struct vnode *, off_t, off_t, kauth_cred_t);
 
 #define VOP_REMOVE_DESCOFFSET 24
-struct vop_remove_v2_args {
+struct vop_remove_v3_args {
 	const struct vnodeop_desc *a_desc;
 	struct vnode *a_dvp;
 	struct vnode *a_vp;
 	struct componentname *a_cnp;
+	nlink_t ctx_vp_new_nlink;
 };
 extern const struct vnodeop_desc vop_remove_desc;
 int VOP_REMOVE(struct vnode *, struct vnode *, struct componentname *);

Reply via email to