/* Copyright (c) 2015 Leon Baker This projected is licensed under the terms of the MIT License. gomilter Go Bindings for libmilter gomilter.go */ package gomilter /* #cgo LDFLAGS: -lmilter #include #include #include #include "libmilter/mfapi.h" #include "filter.h" */ import "C" import ( "bytes" "encoding/binary" "encoding/gob" "errors" "fmt" "log" "net" "os" "reflect" "strings" "unsafe" ) type sockaddr_in struct { sin_family int8 sin_port uint8 sin_addr uint32 sin_zero uint64 } // Return values for Callback functions const ( Continue = iota Reject Discard Accept Tempfail _ _ Noreply Skip ) // flags const ( ADDHDRS = 0x00000001 // 000000001 CHGBODY = 0x00000002 // 000000010 ADDRCPT = 0x00000004 // 000000100 DELRCPT = 0x00000008 // 000001000 CHGHDRS = 0x00000010 // 000010000 QUARANTINE = 0x00000020 // 000100000 CHGFROM = 0x00000040 // 001000000 ADDRCPT_PAR = 0x00000080 // 010000000 SETSYMLIST = 0x00000100 // 100000000 ) // Interface that must be implemented in order to use gomilter type Milter interface { GetFilterName() string GetDebug() bool GetFlags() int GetSocket() string GetLogger() *log.Logger } // An "empty" Milter with no callback functions type MilterRaw struct { FilterName string Debug bool Flags int Socket string Logger *log.Logger } func (m *MilterRaw) GetFilterName() string { return m.FilterName } func (m *MilterRaw) GetDebug() bool { return m.Debug } func (m *MilterRaw) GetFlags() int { return m.Flags } func (m *MilterRaw) GetSocket() string { return m.Socket } func (m *MilterRaw) GetLogger() *log.Logger { return m.Logger } // ********* Callback checking types ********* type checkForConnect interface { Connect(ctx uintptr, hostname string, ip net.IP) (sfsistat int8) } type checkForHelo interface { Helo(ctx uintptr, helohost string) (sfsistat int8) } type checkForEnvFrom interface { EnvFrom(ctx uintptr, myargv []string) (sfsistat int8) } type checkForEnvRcpt interface { EnvRcpt(ctx uintptr, myargv []string) (sfsistat int8) } type checkForHeader interface { Header(ctx uintptr, headerf, headerv string) (sfsistat int8) } type checkForEoh interface { Eoh(ctx uintptr) (sfsistat int8) } type checkForBody interface { Body(ctx uintptr, body []byte) (sfsistat int8) } type checkForEom interface { Eom(ctx uintptr) (sfsistat int8) } type checkForAbort interface { Abort(ctx uintptr) (sfsistat int8) } type checkForClose interface { Close(ctx uintptr) (sfsistat int8) } // ********* Global Milter variable ********* var milter Milter var logger *log.Logger // ********* Utility Functions (not exported) ********* type CtxPtr *C.struct_smfi_str func int2ctx(ptr uintptr) CtxPtr { return CtxPtr(unsafe.Pointer(ptr)) } func ctx2int(ctx CtxPtr) uintptr { return uintptr(unsafe.Pointer(ctx)) } func cStringArrayToSlice(argv **C.char) (GoArgv []string) { // Build a slice of pointers with C array as a backing length := int(C.argv_len(argv)) hdr := reflect.SliceHeader{ Data: uintptr(unsafe.Pointer(argv)), Len: length, Cap: length, } argvSlice := *(*[]*C.char)(unsafe.Pointer(&hdr)) // Build a string slice for Go friendly strings GoArgv = make([]string, length, length) for i := 0; i < length; i++ { GoArgv[i] = C.GoString(argvSlice[i]) } return } func GobEncode(data interface{}) ([]byte, error) { w := new(bytes.Buffer) encoder := gob.NewEncoder(w) err := encoder.Encode(data) if err != nil { return nil, err } return w.Bytes(), nil } func GobDecode(buf []byte, data interface{}) error { r := bytes.NewBuffer(buf) decoder := gob.NewDecoder(r) err := decoder.Decode(data) if err != nil { return err } return nil } // ********* Filter Callback functions ********* // These are registered with Milter // They are only called if they get registered but need to be defined anyway //export Go_xxfi_connect func Go_xxfi_connect(ctx *C.SMFICTX, hostname *C.char, hostaddr *C._SOCK_ADDR) (sfsistat C.sfsistat) { defer func(sfsistat *C.sfsistat) { if r := recover(); r != nil { LoggerPrintf("Panic caught in Go_xxfi_connect(): %s", r) *sfsistat = 75 // tempfail } }(&sfsistat) ctxptr := ctx2int(ctx) var ip net.IP if hostaddr.sa_family == C.AF_INET { hostaddrin := (*sockaddr_in)(unsafe.Pointer(hostaddr)) ip_addr := make([]byte, 4) binary.LittleEndian.PutUint32(ip_addr, hostaddrin.sin_addr) ip = net.IPv4(ip_addr[0], ip_addr[1], ip_addr[2], ip_addr[3]) } else if hostaddr.sa_family == C.AF_INET6 { sa_in := (*C.struct_sockaddr_in6)(unsafe.Pointer(hostaddr)) ip = net.IP(C.GoBytes(unsafe.Pointer(&sa_in.sin6_addr), 16)) } else { if milter.GetDebug() { logger.Println("hostaddr.sa_family value not implemented") } ip = net.ParseIP("::") } m := milter.(checkForConnect) code := m.Connect(ctxptr, C.GoString(hostname), ip) if milter.GetDebug() { logger.Printf("Connect callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_helo func Go_xxfi_helo(ctx *C.SMFICTX, helohost *C.char) C.sfsistat { m := milter.(checkForHelo) code := m.Helo(ctx2int(ctx), C.GoString(helohost)) if milter.GetDebug() { logger.Printf("Helo callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_envfrom func Go_xxfi_envfrom(ctx *C.SMFICTX, argv **C.char) C.sfsistat { // Call our application's callback m := milter.(checkForEnvFrom) code := m.EnvFrom(ctx2int(ctx), cStringArrayToSlice(argv)) if milter.GetDebug() { logger.Printf("EnvFrom callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_envrcpt func Go_xxfi_envrcpt(ctx *C.SMFICTX, argv **C.char) C.sfsistat { // Call our application's callback m := milter.(checkForEnvRcpt) code := m.EnvRcpt(ctx2int(ctx), cStringArrayToSlice(argv)) if milter.GetDebug() { logger.Printf("EnvRcpt callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_header func Go_xxfi_header(ctx *C.SMFICTX, headerf, headerv *C.char) C.sfsistat { m := milter.(checkForHeader) code := m.Header(ctx2int(ctx), C.GoString(headerf), C.GoString(headerv)) if milter.GetDebug() { logger.Printf("Header callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_eoh func Go_xxfi_eoh(ctx *C.SMFICTX) C.sfsistat { // Call our application's callback m := milter.(checkForEoh) code := m.Eoh(ctx2int(ctx)) if milter.GetDebug() { logger.Printf("Eoh callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_body func Go_xxfi_body(ctx *C.SMFICTX, bodyp *C.uchar, bodylen C.size_t) C.sfsistat { // Create a byte slice from the body pointer. // The body pointer just points to a sequence of bytes // Our bit slice is backed by the original body. No copy is made here // Be aware that converting the byte slice to string will make a copy var b []byte b = (*[1 << 30]byte)(unsafe.Pointer(bodyp))[0:bodylen] // Call our application's callback m := milter.(checkForBody) code := m.Body(ctx2int(ctx), b) if milter.GetDebug() { logger.Printf("Body callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_eom func Go_xxfi_eom(ctx *C.SMFICTX) C.sfsistat { // Call our application's callback m := milter.(checkForEom) code := m.Eom(ctx2int(ctx)) if milter.GetDebug() { logger.Printf("Eom callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_abort func Go_xxfi_abort(ctx *C.SMFICTX) C.sfsistat { // Call our application's callback m := milter.(checkForAbort) code := m.Abort(ctx2int(ctx)) if milter.GetDebug() { logger.Printf("Abort callback returned: %d\n", code) } return C.sfsistat(code) } //export Go_xxfi_close func Go_xxfi_close(ctx *C.SMFICTX) C.sfsistat { // Call our application's callback m := milter.(checkForClose) code := m.Close(ctx2int(ctx)) if milter.GetDebug() { logger.Printf("Close callback returned: %d\n", code) } return C.sfsistat(code) } // ********* libmilter Data Access Functions ********* func GetSymVal(ctx uintptr, symname string) string { csymname := C.CString(symname) defer C.free(unsafe.Pointer(csymname)) type CtxPtr *C.struct_smfi_str cval := C.smfi_getsymval(int2ctx(ctx), csymname) // Note: If you try to free the cval C string it will crash return C.GoString(cval) } // Beware that if your struct is too large it will be discarded // without storing. You may want to simply provide a reference // to something you keep in a map or similar structure yourself. func SetPriv(ctx uintptr, privatedata interface{}) error { // privatedata seems to work for any data type // Structs must have exported fields // Serialize Go privatedata into a byte slice bytedata, err := GobEncode(privatedata) if err != nil { return err } return SetPrivBytes(ctx, bytedata) } // See also: http://bit.ly/1HVWA9I func SetPrivBytes(ctx uintptr, bytedata []byte) error { // length and size // length is a uint32 (usually 4 bytes) // the length will be stored in front of the byte sequence length := uint32(len(bytedata)) lengthsize := C.size_t(unsafe.Sizeof(length)) buf := new(bytes.Buffer) err := binary.Write(buf, binary.BigEndian, length) if err != nil { return errors.New("Could not write binary data into buffer: " + err.Error()) } lengthbytes := buf.Bytes() // Allocate memory for the length and byte sequence CArray := (*C.uchar)(C.malloc(lengthsize + C.size_t(length))) var lenStart, seqStart uintptr lenStart = uintptr(unsafe.Pointer(CArray)) seqStart = lenStart + uintptr(lengthsize) CArray = (*C.uchar)(unsafe.Pointer(lenStart)) for i := uintptr(0); i < uintptr(lengthsize); i++ { CArray = (*C.uchar)(unsafe.Pointer(lenStart + i)) *CArray = C.uchar(lengthbytes[i]) } // Now copy the data bytes to the position after the length for i := uintptr(0); i < uintptr(length); i++ { CArray = (*C.uchar)(unsafe.Pointer(seqStart + i)) *CArray = C.uchar(bytedata[i]) } // Call libmilter smfi_setpriv type CtxPtr *C.struct_smfi_str res := int(C.smfi_setpriv(int2ctx(ctx), unsafe.Pointer(lenStart))) if res != int(C.MI_SUCCESS) { return errors.New("smfi_setpriv() returned a failure") } return nil } func GetPriv(ctx uintptr, privatedata interface{}) error { databytes, err := GetPrivBytes(ctx) if err != nil { return err } err = GobDecode(databytes, privatedata) if err != nil { return err } return nil } func GetPrivBytes(ctx uintptr) ([]byte, error) { /* Retrieve the private data stored by the milter Retrieving the data will release the memory allocated for it Don't try to retrieve it again unless you call SetPriv first */ // Call libmilter smfi_getpriv to get a pointer to our data CArray := (*byte)(C.smfi_getpriv(int2ctx(ctx))) // Make sure data has been set with a previous call to SetPriv if CArray == nil { return nil, errors.New("smfi_getpriv() call failed") } // Read uint32 size bytes from the start of the pointer var length uint32 lengthsize := unsafe.Sizeof(length) lengthbytes := make([]byte, lengthsize) lenStart := uintptr(unsafe.Pointer(CArray)) seqStart := lenStart + uintptr(lengthsize) for i := uintptr(0); i < uintptr(lengthsize); i++ { CArray = (*byte)(unsafe.Pointer(lenStart + i)) lengthbytes[i] = *CArray } // Binary decode the length bytes buf := bytes.NewBuffer(lengthbytes) err := binary.Read(buf, binary.BigEndian, &length) if err != nil { return nil, errors.New("Could not parse binary data") } // Read byte sequence of data databytes := make([]byte, length) for i := uintptr(0); i < uintptr(length); i++ { CArray = (*byte)(unsafe.Pointer(seqStart + i)) databytes[i] = *CArray } // Free the data malloc'ed by C C.free(unsafe.Pointer(lenStart)) C.smfi_setpriv(int2ctx(ctx), nil) // Unserialize the data bytes back into a data structure return databytes, nil } func SetReply(ctx uintptr, rcode, xcode, message string) int { type CtxPtr *C.struct_smfi_str crcode := C.CString(rcode) defer C.free(unsafe.Pointer(crcode)) cxcode := C.CString(xcode) defer C.free(unsafe.Pointer(cxcode)) cmessage := C.CString(message) defer C.free(unsafe.Pointer(cmessage)) // Call libmilter setreply return int(C.smfi_setreply(int2ctx(ctx), crcode, cxcode, cmessage)) } func SetMLReply(ctx uintptr, rcode, xcode string, message ...string) int { /* Allows up to 32 lines of message */ crcode := C.CString(rcode) defer C.free(unsafe.Pointer(crcode)) cxcode := C.CString(xcode) defer C.free(unsafe.Pointer(cxcode)) // Build message C array // Get size of a C pointer on this system ptrSize := unsafe.Sizeof(crcode) // Allocate the char** array argv := C.malloc(C.size_t(len(message)) * C.size_t(ptrSize)) defer C.free(argv) // Assign each line to its address offset for i := 0; i < len(message); i++ { linePtr := (**C.char)(unsafe.Pointer(uintptr(argv) + uintptr(i)*ptrSize)) line := C.CString(message[i]) defer C.free(unsafe.Pointer(line)) *linePtr = line } // Call our C wrapper for setmlreply // The wrapper is needed as cgo doesn't support variadic C function calls return int(C.wrap_setmlreply(int2ctx(ctx), crcode, cxcode, C.int(len(message)), (**C.char)(argv))) } // ********* libmilter Message Modification Functions ********* func AddHeader(ctx uintptr, headerf, headerv string) int { /* Add a header to the message. SMFIF_ADDHDRS */ cheaderf := C.CString(headerf) defer C.free(unsafe.Pointer(cheaderf)) cheaderv := C.CString(headerv) defer C.free(unsafe.Pointer(cheaderv)) // Call smfi_addheader return int(C.smfi_addheader(int2ctx(ctx), cheaderf, cheaderv)) } func ChgHeader(ctx uintptr, headerf string, hdridx int, headerv string) int { /* Change or delete a header. SMFIF_CHGHDRS */ cheaderf := C.CString(headerf) defer C.free(unsafe.Pointer(cheaderf)) cheaderv := C.CString(headerv) defer C.free(unsafe.Pointer(cheaderv)) // Call smfi_chgheader return int(C.smfi_chgheader(int2ctx(ctx), cheaderf, C.int(hdridx), cheaderv)) } func InsHeader(ctx uintptr, hdridx int, headerf, headerv string) int { /* Insert a header into the message. SMFIF_ADDHDRS */ cheaderf := C.CString(headerf) defer C.free(unsafe.Pointer(cheaderf)) cheaderv := C.CString(headerv) defer C.free(unsafe.Pointer(cheaderv)) // Call smfi_insheader return int(C.smfi_insheader(int2ctx(ctx), C.int(hdridx), cheaderf, cheaderv)) } func ChgFrom(ctx uintptr, mail, args string) int { /* Change the envelope sender address. SMFIF_CHGFROM */ cmail := C.CString(mail) defer C.free(unsafe.Pointer(cmail)) cargs := C.CString(args) defer C.free(unsafe.Pointer(cargs)) return int(C.smfi_chgfrom(int2ctx(ctx), cmail, cargs)) } func AddRcpt(ctx uintptr, rcpt string) int { /* Add a recipient to the envelope. SMFIF_ADDRCPT */ crcpt := C.CString(rcpt) defer C.free(unsafe.Pointer(crcpt)) // Call smfi_addrcpt return int(C.smfi_addrcpt(int2ctx(ctx), crcpt)) } func AddRcpt_Par(ctx uintptr, rcpt, args string) int { /* Add a recipient including ESMTP parameter to the envelope. SMFIF_ADDRCPT_PAR */ crcpt := C.CString(rcpt) defer C.free(unsafe.Pointer(crcpt)) cargs := C.CString(args) defer C.free(unsafe.Pointer(cargs)) // Call smfi_addrcpt return int(C.smfi_addrcpt_par(int2ctx(ctx), crcpt, cargs)) } func DelRcpt(ctx uintptr, rcpt string) int { /* Delete a recipient from the envelope. SMFIF_DELRCPT */ crcpt := C.CString(rcpt) defer C.free(unsafe.Pointer(crcpt)) // Call smfi_addrcpt return int(C.smfi_delrcpt(int2ctx(ctx), crcpt)) } func ReplaceBody(ctx uintptr, body []byte) int { /* Replace the body of the message. SMFIF_CHGBODY */ // Allocate memory length := len(body) // Allocate memory for the length and byte sequence cbody := (*C.uchar)(C.malloc(C.size_t(length))) start := uintptr(unsafe.Pointer(cbody)) for i := uintptr(0); i < uintptr(length); i++ { cbody = (*C.uchar)(unsafe.Pointer(start + i)) *cbody = C.uchar(body[i]) } cbody = (*C.uchar)(unsafe.Pointer(start)) // Call smfi_replacebody return int(C.smfi_replacebody(int2ctx(ctx), cbody, C.int(length))) } // ********* Other Message Handling Functions ********* func progress(ctx uintptr) int { /* Report operation in progress. */ // Call smfi_progress return int(C.smfi_progress(int2ctx(ctx))) } // ********* Run the milter ********* func Run(amilter Milter) int { milter = amilter // Setup logging logger = milter.GetLogger() if logger == nil { // No logger defined so create a simple logger to Stdout logger = log.New(os.Stdout, "", 0) } if milter.GetDebug() { logger.Println("Debugging enabled") } // Declare an empty smfiDesc structure var smfilter C.smfiDesc_str // Set filter name fname := C.CString(milter.GetFilterName()) defer C.free(unsafe.Pointer(fname)) smfilter.xxfi_name = fname if milter.GetDebug() { logger.Printf("Filter Name: %s\n", C.GoString(smfilter.xxfi_name)) } // Set version code smfilter.xxfi_version = C.SMFI_VERSION // Set Flags smfilter.xxfi_flags = C.ulong(milter.GetFlags()) if milter.GetDebug() { logger.Printf("Flags: 0x%b\n", smfilter.xxfi_flags) } // Set Callbacks if they are implemented // Check if Connect method was implemented if _, ok := milter.(checkForConnect); ok { if milter.GetDebug() { logger.Println("Connect callback implemented") } C.setConnect(&smfilter) } else { if milter.GetDebug() { logger.Println("Connect callback not implemented") } } // Check if Helo method was implemented if _, ok := milter.(checkForHelo); ok { if milter.GetDebug() { logger.Println("Helo callback implemented") } C.setHelo(&smfilter) } else { if milter.GetDebug() { logger.Println("Helo callback not implemented") } } // Check if EnvFrom method was implemented if _, ok := milter.(checkForEnvFrom); ok { if milter.GetDebug() { logger.Println("EnvFrom callback implemented") } C.setEnvFrom(&smfilter) } else { if milter.GetDebug() { logger.Println("EnvFrom callback not implemented") } } // Check if EnvRcpt method was implemented if _, ok := milter.(checkForEnvRcpt); ok { if milter.GetDebug() { logger.Println("EnvRcpt callback implemented") } C.setEnvRcpt(&smfilter) } else { if milter.GetDebug() { logger.Println("EnvRcpt callback not implemented") } } // Check if Header method was implemented if _, ok := milter.(checkForHeader); ok { if milter.GetDebug() { logger.Println("Header callback implemented") } C.setHeader(&smfilter) } else { if milter.GetDebug() { logger.Println("Header callback not implemented") } } // Check if Eoh method was implemented if _, ok := milter.(checkForEoh); ok { if milter.GetDebug() { logger.Println("Eoh callback implemented") } C.setEoh(&smfilter) } else { if milter.GetDebug() { logger.Println("Eoh callback not implemented") } } // Check if Body method was implemented if _, ok := milter.(checkForBody); ok { if milter.GetDebug() { logger.Println("Body callback implemented") } C.setBody(&smfilter) } else { if milter.GetDebug() { logger.Println("Body callback not implemented") } } // Check if Eom method was implemented if _, ok := milter.(checkForEom); ok { if milter.GetDebug() { logger.Println("Eom callback implemented") } C.setEom(&smfilter) } else { if milter.GetDebug() { logger.Println("Eom callback not implemented") } } // Check if Abort method was implemented if _, ok := milter.(checkForAbort); ok { if milter.GetDebug() { logger.Println("Abort callback implemented") } C.setAbort(&smfilter) } else { if milter.GetDebug() { logger.Println("Abort callback not implemented") } } // Check if Close method was implemented if _, ok := milter.(checkForClose); ok { if milter.GetDebug() { logger.Println("Close callback implemented") } C.setClose(&smfilter) } else { if milter.GetDebug() { logger.Println("Close callback not implemented") } } if milter.GetDebug() { logger.Println("smfilter:") logger.Println(fmt.Sprint(smfilter)) } // Setup socket connection socket := milter.GetSocket() if socket == "" { panic("No socket name. Set MilterRaw.Socket") } // Try to delete old socket if it exists socketparts := strings.Split(socket, ":") if len(socketparts) == 2 { os.Remove(socketparts[1]) } csocket := C.CString(socket) defer C.free(unsafe.Pointer(csocket)) if code := C.smfi_setconn(csocket); code != 0 { logger.Printf("smfi_setconn failed: %d\n", code) } // Register the filter if code := C.smfi_register(smfilter); code == C.MI_FAILURE { logger.Printf("smfi_register failed: %d\n", code) } // Hand control to libmilter if milter.GetDebug() { logger.Println("Handing over to libmilter") } result := C.smfi_main() if milter.GetDebug() { logger.Printf("smfi_main returned: %v\n", result) } return int(result) } func Stop() { C.smfi_stop() }