基于Golang与inotify实现Postfix邮件写入完成触发的问题
解决Postfix接收邮件触发Golang代码的优雅方案
你猜的没错——监控目录时,IN_CLOSE_WRITE事件只会针对目录本身的操作触发,不会感知目录内文件的写入完成。结合Maildir的工作原理,其实根本不需要监控文件的写入事件,用下面的方法就能完美规避竞态和sleep的问题:
核心原理:利用Maildir的原子性移动特性
Postfix严格遵循Maildir规范:
- 接收邮件时,先在
Maildir/tmp目录创建临时文件,完全写入后,通过操作系统的原子移动操作将文件移到Maildir/new目录 - 原子移动意味着:要么文件完整出现在
new目录,要么完全不存在,不存在中间状态
所以只要new目录里出现文件,就代表邮件已经写入完成,直接处理即可。
具体实现步骤
- 用
fsnotify包监控Maildir/new目录的IN_MOVED_TO和IN_CREATE事件IN_MOVED_TO对应Postfix从tmp移动过来的文件(主要场景)IN_CREATE作为兼容,覆盖某些直接在new创建文件的特殊MTA情况
- 处理文件时遵循Maildir规范
- 先将文件从
new目录移动到cur目录(可以加上:2,这类状态标记),避免后续重复处理 - 再读取
cur目录下的文件内容,执行你的业务逻辑
- 先将文件从
Golang代码示例
package main import ( "fmt" "os" "path/filepath" "github.com/fsnotify/fsnotify" ) func main() { maildirPath := "/path/to/your/Maildir" newDir := filepath.Join(maildirPath, "new") curDir := filepath.Join(maildirPath, "cur") // 创建监控器 watcher, err := fsnotify.NewWatcher() if err != nil { fmt.Printf("创建监控器失败: %v\n", err) return } defer watcher.Close() // 监控new目录 err = watcher.Add(newDir) if err != nil { fmt.Printf("添加目录监控失败: %v\n", err) return } fmt.Printf("开始监控Maildir/new目录: %s\n", newDir) for { select { case event, ok := <-watcher.Events: if !ok { return } // 只处理文件,跳过目录本身的事件 if (event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Move == fsnotify.Move) { if info, err := os.Stat(event.Name); err == nil && !info.IsDir() { // 生成cur目录下的目标路径,添加Maildir状态标记 filename := filepath.Base(event.Name) curFilePath := filepath.Join(curDir, fmt.Sprintf("%s:2,", filename)) // 原子移动文件到cur目录 err = os.Rename(event.Name, curFilePath) if err != nil { fmt.Printf("移动文件失败: %v\n", err) continue } // 读取邮件内容并处理 content, err := os.ReadFile(curFilePath) if err != nil { fmt.Printf("读取邮件失败: %v\n", err) continue } fmt.Println("收到新邮件,内容长度:", len(content)) // 在这里执行你的业务逻辑,比如解析邮件、调用接口等 } } case err, ok := <-watcher.Errors: if !ok { return } fmt.Printf("监控错误: %v\n", err) } } }
关键细节说明
- 为什么不需要监控
tmp目录?因为Postfix在tmp里的文件可能还在写入,而且可能会被删除(比如投递失败),直接监控new更可靠 - 原子移动操作由操作系统保证,不会出现文件还在写入就被处理的情况,彻底规避竞态条件
- 移动到
cur目录是Maildir规范要求,确保即使程序崩溃重启,也不会重复处理同一封邮件
内容的提问来源于stack exchange,提问作者Vitaliy
相关产品推荐
相关产品推荐

