Esempio n. 1
0
    int
main( int ac, char *av[ ] )
{
    yastr		test_str;
    struct dmarc	*d;

    if (( ac < 2 ) || ( ac > 5 )) {
	fprintf( stderr, "Usage:\t\t%s hostname [ 5322.From domain ] [ SPF domain ] [ DKIM domain ]\n", av[ 0 ] );
	exit( 1 );
    }

    if ( simta_read_config( SIMTA_FILE_CONFIG ) < 0 ) {
	exit( 1 );
    }

    if ( simta_config( ) != 0 ) {
	exit( 1 );
    }

    simta_openlog( 0, LOG_PERROR );

    dmarc_init( &d );

    dmarc_lookup( d, av[ 1 ] );

    printf( "DMARC lookup result: policy %s, percent %d, result %s\n",
	    dmarc_result_str( d->policy ), d->pct,
	    dmarc_result_str( d->result ));

    if ( ac > 2 ) {
	d->domain = av[ 2 ];
    }

    test_str = yaslauto( d->domain );

    if ( ac > 3 ) {
	dmarc_spf_result( d, av[ 3 ] );
	test_str = yaslcat( test_str, "/" );
	test_str = yaslcat( test_str, av[ 3 ] );
    }
    if ( ac > 4 ) {
	dmarc_dkim_result( d, av[ 4 ] );
	test_str = yaslcat( test_str, "/" );
	test_str = yaslcat( test_str, av[ 4 ] );
    }

    printf( "DMARC policy result for %s: %s\n", test_str,
	    dmarc_result_str( dmarc_result( d )));

    exit( 0 );
}
Esempio n. 2
0
    struct spf *
spf_lookup( const char *helo, const char *email, const struct sockaddr *addr )
{
    char		    *p;
    struct spf		    *s;

    s = calloc( 1, sizeof( struct spf ));
    s->spf_queries = 0;
    s->spf_sockaddr = addr;
    s->spf_helo = yaslauto( helo );

    if ( strlen( email ) == 0 ) {
	/* RFC 7208 2.4 The "MAIL FROM" Identity
	 * When the reverse-path is null, this document defines the "MAIL FROM"
	 * identity to be the mailbox composed of the local-part "postmaster"
	 * and the "HELO" identity
	 */
	s->spf_domain = yaslauto( helo );
	s->spf_localpart = yaslauto( "postmaster" );
    } else if (( p = strrchr( email, '@' )) != NULL ) {
	s->spf_domain = yaslauto( p + 1 );
	s->spf_localpart = yaslnew( email, (size_t) (p - email ));
    } else {
	/* RFC 7208 4.3 Initial Processing
	 * If the <sender> has no local-part, substitute the string
	 * "postmaster" for the local-part.
	 */
	s->spf_domain = yaslauto( email );
	s->spf_localpart = yaslauto( "postmaster" );
    }

    simta_debuglog( 2, "SPF %s: localpart %s", s->spf_domain, s->spf_localpart );

    s->spf_result = spf_check_host( s, s->spf_domain );

    return( s );
}
Esempio n. 3
0
    yastr
env_dkim_sign( struct envelope *env )
{
    char	    df[ MAXPATHLEN + 1 ];
    DKIM_LIB	    *libhandle;
    unsigned int    flags;
    DKIM	    *dkim;
    DKIM_STAT	    result;
    yastr	    signature = NULL;
    yastr	    key = NULL;
    yastr	    tmp = NULL;
    yastr	    *split = NULL;
    size_t	    tok_count = 0;
    char	    buf[ 1024 * 1024 ];
    unsigned char   *dkim_header;
    SNET	    *snet;
    ssize_t	    chunk;

    sprintf( df, "%s/D%s", env->e_dir, env->e_id );

    if (( snet = snet_open( simta_dkim_key, O_RDONLY, 0,
	    1024 * 1024 )) == NULL ) {
	syslog( LOG_ERR, "Liberror: env_dkim_sign snet_open %s: %m",
		simta_dkim_key );
	return( NULL );
    }
    key = yaslempty();
    while (( chunk = snet_read( snet, buf, 1024 * 1024, NULL )) > 0 ) {
	key = yaslcatlen( key, buf, chunk );
    }
    snet_close( snet );

    if (( libhandle = dkim_init( NULL, NULL )) == NULL ) {
	syslog( LOG_ERR, "Liberror: env_dkim_sign dkim_init" );
	return( NULL );
    }

    /* Data is stored in UNIX format, so tell libopendkim to fix
     * CRLF issues.
     */
    flags = DKIM_LIBFLAGS_FIXCRLF;
    dkim_options( libhandle, DKIM_OP_SETOPT, DKIM_OPTS_FLAGS, &flags,
	    sizeof( flags ));

    /* Only sign the headers recommended by RFC 6376 */
    dkim_options( libhandle, DKIM_OP_SETOPT, DKIM_OPTS_SIGNHDRS,
	    dkim_should_signhdrs, sizeof( unsigned char ** ));

    if (( dkim = dkim_sign( libhandle, (unsigned char *)(env->e_id), NULL,
	    (unsigned char *)key, (unsigned char *)simta_dkim_selector,
	    (unsigned char *)simta_dkim_domain,
	    DKIM_CANON_RELAXED, DKIM_CANON_RELAXED, DKIM_SIGN_RSASHA256,
	    -1, &result )) == NULL ) {
	syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_sign: %s",
		dkim_getresultstr( result ));
	goto error;
    }

    if (( snet = snet_open( df, O_RDONLY, 0, 1024 * 1024 )) == NULL ) {
	syslog( LOG_ERR, "Liberror: env_dkim_sign snet_open %s: %m",
		buf );
	goto error;
    }

    while (( chunk = snet_read( snet, buf, 1024 * 1024, NULL )) > 0 ) {
	if (( result = dkim_chunk( dkim, (unsigned char *)buf,
		chunk )) != 0 ) {
	    syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_chunk: %s: %s",
		    dkim_getresultstr( result ),
		    dkim_geterror( dkim ));
	    snet_close( snet );
	    goto error;
	}
    }

    snet_close( snet );

    if (( result = dkim_chunk( dkim, NULL, 0 )) != 0 ) {
	syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_chunk: %s: %s",
		dkim_getresultstr( result ),
		dkim_geterror( dkim ));
	goto error;
    }
    if (( result = dkim_eom( dkim, NULL )) != 0 ) {
	syslog( LOG_NOTICE, "Liberror: env_dkim_sign dkim_eom: %s: %s",
		dkim_getresultstr( result ),
		dkim_geterror( dkim ));
	goto error;
    }
    if (( result = dkim_getsighdr_d( dkim, 16, &dkim_header,
	    (size_t *)&chunk )) != 0 ) {
	syslog( LOG_NOTICE,
		"Liberror: env_dkim_sign dkim_getsighdr_d: %s: %s",
		dkim_getresultstr( result ),
		dkim_geterror( dkim ));
	goto error;
    }

    /* Get rid of carriage returns in libopendkim output */
    split = yaslsplitlen( (const char *)dkim_header,
	    strlen( (const char *)dkim_header ), "\r", 1, &tok_count );
    tmp = yasljoinyasl( split, tok_count, "", 0 );
    signature = yaslcatyasl( yaslauto( "DKIM-Signature: " ), tmp );

error:
    yaslfree( tmp );
    yaslfreesplitres( split, tok_count );
    yaslfree( key );
    dkim_free( dkim );
    dkim_close( libhandle );

    return( signature );
}
Esempio n. 4
0
    int
spf_check_host( struct spf *s, const yastr domain )
{
    int			    i, j, rc, qualifier, ret = SPF_RESULT_NONE;
    struct dnsr_result	    *dnsr_res, *dnsr_res_mech = NULL;
    struct dnsr_string      *txt;
    yastr		    record = NULL, redirect = NULL, domain_spec, tmp;
    size_t		    tok_count = 0;
    yastr		    *split = NULL;
    char		    *p;
    unsigned long	    cidr, cidr6;
    int			    mech_queries = 0;

    /* RFC 7208 3.1 DNS Resource Records
     * SPF records MUST be published as a DNS TXT (type 16) Resource Record
     * (RR) [RFC1035] only.
     */
    if (( dnsr_res = get_txt( domain )) == NULL ) {
	syslog( LOG_WARNING, "SPF %s [%s]: TXT lookup %s failed",
		s->spf_domain, domain, domain );
	return( SPF_RESULT_TEMPERROR );
    }

    for ( i = 0 ; i < dnsr_res->r_ancount ; i++ ) {
	if ( dnsr_res->r_answer[ i ].rr_type == DNSR_TYPE_TXT ) {
	    txt = dnsr_res->r_answer[ i ].rr_txt.txt_data;
	    /* RFC 7208 4.5 Selecting Records
	     * Starting with the set of records that were returned by the
	     * lookup, discard records that do not begin with a version section
	     * of exactly "v=spf1".  Note that the version section is
	     * terminated by either an SP character or the end of the record.
	     */
	    if (( strncasecmp( txt->s_string, "v=spf1", 6 ) == 0 ) &&
		    (( txt->s_string[ 6 ] == ' ' ) ||
		    ( txt->s_string[ 6 ] == '\0' ))) {
		if ( record != NULL ) {
		    /* RFC 7208 3.2 Multiple DNS Records
		     * A domain name MUST NOT have multiple records that would
		     * cause an authorization check to select more than one
		     * record.
		     */
		    syslog( LOG_ERR,
			    "SPF %s [%s]: multiple v=spf1 records found",
			    s->spf_domain, domain );
		    ret = SPF_RESULT_PERMERROR;
		    goto cleanup;
		}
		record = yaslempty( );
		/* RFC 7208 3.3 Multiple Strings in a Single DNS Record
		 * If a published record contains multiple character-strings,
		 * then the record MUST be treated as if those strings are
		 * concatenated together without adding spaces.
		 */
		for ( ; txt != NULL ; txt = txt->s_next ) {
		    record = yaslcat( record, txt->s_string );
		}
	    }
	}
    }

    if ( record == NULL ) {
	simta_debuglog( 1, "SPF %s [%s]: no SPF record found",
		s->spf_domain, domain );
	goto cleanup;
    }

    simta_debuglog( 2, "SPF %s [%s]: record: %s", s->spf_domain, domain, record );

    split = yaslsplitlen( record, yasllen( record ), " ", 1, &tok_count );

    /* Start at 1, 0 is v=spf1 */
    for ( i = 1 ; i < tok_count ; i++ ) {
	/* multiple spaces in a record will result in empty elements */
	if ( yasllen( split[ i ] ) == 0 ) {
	    continue;
	}


	/* RFC 7208 4.6.4 DNS Lookup Limits
	 * Some mechanisms and modifiers (collectively, "terms") cause DNS
	 * queries at the time of evaluation [...] SPF implementations MUST
	 * limit the total number of those terms to 10 during SPF evaluation,
	 * to avoid unreasonable load on the DNS.  If this limit is exceeded,
	 * the implementation MUST return "permerror".
	 */
	/* In real life strictly enforcing a limit of ten will break SPF
	 * evaluation of multiple major domains, so we use a higher limit.
	 */
	if ( s->spf_queries > 25 ) {
	    syslog( LOG_WARNING, "SPF %s [%s]: DNS lookup limit exceeded",
		    s->spf_domain, domain );
	    ret = SPF_RESULT_PERMERROR;
	    goto cleanup;
	}

	/* RFC 7208 4.6.2 Mechanisms
	 * The possible qualifiers, and the results they cause check_host() to
	 * return, are as follows:
	 *
	 * "+" pass
	 * "-" fail
	 * "~" softfail
	 * "?" neutral
	 *
	 * The qualifier is optional and defaults to "+".
	 */
	switch ( *split[ i ] ) {
	case '+':
	    qualifier = SPF_RESULT_PASS;
	    yaslrange( split[ i ], 1, -1 );
	    break;
	case '-':
	    qualifier = SPF_RESULT_FAIL;
	    yaslrange( split[ i ], 1, -1 );
	    break;
	case '~':
	    qualifier = SPF_RESULT_SOFTFAIL;
	    yaslrange( split[ i ], 1, -1 );
	    break;
	case '?':
	    qualifier = SPF_RESULT_NEUTRAL;
	    yaslrange( split[ i ], 1, -1 );
	    break;
	default:
	    qualifier = SPF_RESULT_PASS;
	    break;
	}

	if ( strncasecmp( split[ i ], "redirect=", 9 ) == 0 ) {
	    s->spf_queries++;
	    redirect = split[ i ];
	    yaslrange( redirect, 9, -1 );
	    simta_debuglog( 2, "SPF %s [%s]: redirect to %s",
		    s->spf_domain, domain, redirect );

	/* RFC 7208 5.1 "all"
	 * The "all" mechanism is a test that always matches.
	 */
	} else if ( strcasecmp( split[ i ], "all" ) == 0 ) {
	    simta_debuglog( 2, "SPF %s [%s]: matched all: %s",
		    s->spf_domain, domain, spf_result_str( qualifier ));
	    ret = qualifier;
	    goto cleanup;

	/* RFC 7208 5.2 "include"
	 * The "include" mechanism triggers a recursive evaluation of
	 * check_host().
	 */
	} else if ( strncasecmp( split[ i ], "include:", 8 ) == 0 ) {
	    s->spf_queries++;
	    yaslrange( split[ i ], 8, -1 );
	    simta_debuglog( 2, "SPF %s [%s]: include %s",
		    s->spf_domain, domain, split[ i ] );
	    rc = spf_check_host( s, split[ i ] );
	    switch ( rc ) {
	    case SPF_RESULT_NONE:
		ret = SPF_RESULT_PERMERROR;
		goto cleanup;
	    case SPF_RESULT_PASS:
		ret = qualifier;
		goto cleanup;
	    case SPF_RESULT_TEMPERROR:
	    case SPF_RESULT_PERMERROR:
		ret = rc;
		goto cleanup;
	    }

	/* RFC 7208 5.3 "a" */
	} else if (( strcasecmp( split[ i ], "a" ) == 0 ) ||
		( strncasecmp( split[ i ], "a:", 2 ) == 0 ) ||
		( strncasecmp( split[ i ], "a/", 2 ) == 0 )) {
	    s->spf_queries++;
	    yaslrange( split[ i ], 1, -1 );

	    if (( domain_spec = spf_parse_domainspec_cidr( s, domain,
		    split[ i ], &cidr, &cidr6 )) == NULL ) {
		/* Macro expansion failed, probably a syntax problem. */
		ret = SPF_RESULT_PERMERROR;
		goto cleanup;
	    }

	    rc = spf_check_a( s, domain, cidr, cidr6, domain_spec );

	    switch( rc ) {
	    case SPF_RESULT_PASS:
		simta_debuglog( 2, "SPF %s [%s]: matched a %s/%ld/%ld: %s",
			s->spf_domain, domain, domain_spec, cidr, cidr6,
			spf_result_str( qualifier ));
		yaslfree( domain_spec );
		ret = qualifier;
		goto cleanup;
	    case SPF_RESULT_TEMPERROR:
		yaslfree( domain_spec );
		ret = rc;
		goto cleanup;
	    default:
		break;
	    }

	    yaslfree( domain_spec );

	/* RFC 7208 5.4 "mx" */
	} else if (( strcasecmp( split[ i ], "mx" ) == 0 ) ||
		( strncasecmp( split[ i ], "mx:", 3 ) == 0 ) ||
		( strncasecmp( split[ i ], "mx/", 3 ) == 0 )) {
	    s->spf_queries++;
	    mech_queries = 0;
	    yaslrange( split[ i ], 2, -1 );

	    if (( domain_spec = spf_parse_domainspec_cidr( s, domain,
		    split[ i ], &cidr, &cidr6 )) == NULL ) {
		/* Macro expansion failed, probably a syntax problem. */
		ret = SPF_RESULT_PERMERROR;
		goto cleanup;
	    }

	    if (( dnsr_res_mech = get_mx( domain_spec )) == NULL ) {
		syslog( LOG_WARNING, "SPF %s [%s]: MX lookup %s failed",
			s->spf_domain, domain, domain_spec );
		yaslfree( domain_spec );
		ret = SPF_RESULT_TEMPERROR;
		goto cleanup;
	    }

	    for ( j = 0 ; j < dnsr_res_mech->r_ancount ; j++ ) {
		if ( dnsr_res_mech->r_answer[ j ].rr_type == DNSR_TYPE_MX ) {
		    /* RFC 7208 4.6.4 DNS Lookup Limits
		     * When evaluating the "mx" mechanism, the number of "MX"
		     * resource records queried is included in the overall
		     * limit of 10 mechanisms/modifiers that cause DNS lookups
		     */
		    s->spf_queries++;
		    rc = spf_check_a( s, domain, cidr, cidr6,
			    dnsr_res_mech->r_answer[ j ].rr_mx.mx_exchange );
		    switch( rc ) {
		    case SPF_RESULT_PASS:
			simta_debuglog( 2,
				"SPF %s [%s]: matched mx %s/%ld/%ld: %s",
				s->spf_domain, domain, domain_spec, cidr, cidr6,
				spf_result_str( qualifier ));
			ret = qualifier;
			dnsr_free_result( dnsr_res_mech );
			yaslfree( domain_spec );
			goto cleanup;
		    case SPF_RESULT_PERMERROR:
		    case SPF_RESULT_TEMPERROR:
			ret = rc;
			dnsr_free_result( dnsr_res_mech );
			yaslfree( domain_spec );
			goto cleanup;
		    default:
			break;
		    }
		}
	    }

	    dnsr_free_result( dnsr_res_mech );
	    yaslfree( domain_spec );

	/* RFC 7208 5.5 "ptr" (do not use) */
	} else if (( strcasecmp( split[ i ], "ptr" ) == 0 ) ||
		( strncasecmp( split[ i ], "ptr:", 4 ) == 0 )) {
	    s->spf_queries++;
	    mech_queries = 0;
	    if (( dnsr_res_mech = get_ptr( s->spf_sockaddr )) == NULL ) {
		/* RFC 7208 5.5 "ptr" (do not use )
		 * If a DNS error occurs while doing the PTR RR lookup,
		 * then this mechanism fails to match.
		 */
		continue;
	    }

	    if ( dnsr_res_mech->r_ancount == 0 ) {
		dnsr_free_result( dnsr_res_mech );
		continue;
	    }

	    if ( split[ i ][ 3 ] == ':' ) {
		domain_spec = yaslnew(( split[ i ] + 4 ),
			( yasllen( split[ i ] ) - 4 ));
	    } else {
		domain_spec = yasldup( domain );
	    }

	    for ( j = 0 ; j < dnsr_res_mech->r_ancount ; j++ ) {
		if ( dnsr_res_mech->r_answer[ j ].rr_type != DNSR_TYPE_PTR ) {
		    continue;
		}
		/* We only care if it's a pass; like the initial PTR query,
		 * DNS errors are treated as a non-match rather than an error.
		 */
		/* RFC 7208 4.6.4 DNS Lookup Limits
		 * the evaluation of each "PTR" record MUST NOT result in
		 * querying more than 10 address records -- either "A" or
		 * "AAAA" resource records.  If this limit is exceeded, all
		 * records  other than the first 10 MUST be ignored.
		 */
		if (( mech_queries++ < 10 ) && ( spf_check_a( s, domain, 32,
			128, dnsr_res_mech->r_answer[ j ].rr_dn.dn_name ) ==
			SPF_RESULT_PASS )) {
		    tmp = yaslauto(
			    dnsr_res_mech->r_answer[ j ].rr_dn.dn_name );
		    while (( yasllen( tmp ) > yasllen( domain_spec )) &&
			    ( p = strchr( tmp, '.' ))) {
			yaslrange( tmp, ( p - tmp + 1 ), -1 );
		    }
		    rc = strcasecmp( tmp, domain_spec );
		    yaslfree( tmp );
		    if ( rc == 0 ) {
			simta_debuglog( 2,
				"SPF %s [%s]: matched ptr %s (%s): %s",
				s->spf_domain, domain, domain_spec,
				dnsr_res_mech->r_answer[ j ].rr_dn.dn_name,
				spf_result_str( qualifier ));
			ret = qualifier;
			yaslfree( domain_spec );
			dnsr_free_result( dnsr_res_mech );
			goto cleanup;
		    }
		}
	    }

	    yaslfree( domain_spec );
	    dnsr_free_result( dnsr_res_mech );

	/* RFC 7208 5.6 "ip4" and "ip6"
	 * These mechanisms test whether <ip> is contained within a given
	 * IP network.
	 */
	} else if ( strncasecmp( split[ i ], "ip4:", 4 ) == 0 ) {
	    if ( s->spf_sockaddr->sa_family != AF_INET ) {
		continue;
	    }

	    yaslrange( split[ i ], 4, -1 );
	    if (( p = strchr( split[ i ], '/' )) != NULL ) {
		errno = 0;
		cidr = strtoul( p + 1, NULL, 10 );
		if ( errno ) {
		    syslog( LOG_WARNING,
			    "SPF %s [%s]: failed parsing CIDR mask %s: %m",
			    s->spf_domain, domain, p + 1 );
		    ret = SPF_RESULT_PERMERROR;
		    goto cleanup;
		}
		if ( cidr > 32 ) {
		    syslog( LOG_WARNING, "SPF %s [%s]: invalid CIDR mask: %ld",
			    s->spf_domain, domain, cidr );
		    ret = SPF_RESULT_PERMERROR;
		    goto cleanup;
		}
		yaslrange( split[ i ], 0, p - split[ i ] - 1 );
	    } else {
		cidr = 32;
	    }

	    if (( rc = simta_cidr_compare( cidr, s->spf_sockaddr, NULL,
		    split[ i ] )) < 0 ) {
		syslog( LOG_WARNING,
			"SPF %s [%s]: simta_cidr_compare failed for %s",
			s->spf_domain, domain, split[ i ] );
		ret = SPF_RESULT_PERMERROR;
		goto cleanup;
	    } else if ( rc == 0 ) {
		simta_debuglog( 2, "SPF %s [%s]: matched ip4 %s/%ld: %s",
			s->spf_domain, domain, split[ i ], cidr,
			spf_result_str( qualifier ));
		ret = qualifier;
		goto cleanup;
	    }

	} else if ( strncasecmp( split[ i ], "ip6:", 4 ) == 0 ) {
	    if ( s->spf_sockaddr->sa_family != AF_INET6 ) {
		continue;
	    }

	    yaslrange( split[ i ], 4, -1 );
	    if (( p = strchr( split[ i ], '/' )) != NULL ) {
		errno = 0;
		cidr = strtoul( p + 1, NULL, 10 );
		if ( errno ) {
		    syslog( LOG_WARNING,
			    "SPF %s [%s]: failed parsing CIDR mask %s: %m",
			    s->spf_domain, domain, p + 1 );
		}
		if ( cidr > 128 ) {
		    syslog( LOG_WARNING, "SPF %s [%s]: invalid CIDR mask: %ld",
			    s->spf_domain, domain, cidr );
		    ret = SPF_RESULT_PERMERROR;
		    goto cleanup;
		}
		yaslrange( split[ i ], 0, p - split[ i ] - 1 );
	    } else {
		cidr = 128;
	    }

	    if (( rc = simta_cidr_compare( cidr, s->spf_sockaddr, NULL,
		    split[ i ] )) < 0 ) {
		syslog( LOG_WARNING,
			"SPF %s [%s]: simta_cidr_compare failed for %s",
			s->spf_domain, domain, split[ i ] );
		ret = SPF_RESULT_PERMERROR;
		goto cleanup;
	    } else if ( rc == 0 ) {
		simta_debuglog( 2, "SPF %s [%s]: matched ip6 %s/%ld: %s",
			s->spf_domain, domain, split[ i ], cidr,
			spf_result_str( qualifier ));
		ret = qualifier;
		goto cleanup;
	    }

	/* RFC 7208 5.7 "exists" */
	} else if ( strncasecmp( split[ i ], "exists:", 7 ) == 0 ) {
	    s->spf_queries++;
	    yaslrange( split[ i ], 7, -1 );
	    if (( domain_spec =
		    spf_macro_expand( s, domain, split[ i ] )) == NULL ) {
		/* Macro expansion failed, probably a syntax problem. */
		ret = SPF_RESULT_PERMERROR;
		goto cleanup;
	    }

	    if (( dnsr_res_mech = get_a( domain_spec )) == NULL ) {
		syslog( LOG_WARNING, "SPF %s [%s]: A lookup %s failed",
			s->spf_domain, domain, domain_spec );
		yaslfree( domain_spec );
		ret = SPF_RESULT_TEMPERROR;
		goto cleanup;
	    }

	    if ( dnsr_res_mech->r_ancount > 0 ) {
		simta_debuglog( 2, "SPF %s [%s]: matched exists %s: %s",
			s->spf_domain, domain, domain_spec,
			spf_result_str( qualifier ));
		dnsr_free_result( dnsr_res_mech );
		yaslfree( domain_spec );
		ret = qualifier;
		goto cleanup;
	    }

	    yaslfree( domain_spec );
	    dnsr_free_result( dnsr_res_mech );

	} else {
	    for ( p = split[ i ] ; isalnum( *p ) ; p++ );

	    if ( *p == '=' ) {
		/* RFC 7208 6 Modifier Definitions
		 * Unrecognized modifiers MUST be ignored
		 */
		simta_debuglog( 1, "SPF %s [%s]: %s unknown modifier %s",
			s->spf_domain, domain, spf_result_str( qualifier ),
			split[ i ] );
	    } else {
		syslog( LOG_WARNING, "SPF %s [%s]: %s unknown mechanism %s",
			s->spf_domain, domain, spf_result_str( qualifier ),
			split[ i ] );
		ret = SPF_RESULT_PERMERROR;
		goto cleanup;
	    }
	}
    }

    if ( redirect != NULL ) {
	if (( domain_spec = spf_macro_expand( s, domain, redirect )) == NULL ) {
	    /* Macro expansion failed, probably a syntax problem. */
	    ret = SPF_RESULT_PERMERROR;
	} else {
	    ret = spf_check_host( s, domain_spec );
	    yaslfree( domain_spec );
	}
	if ( ret == SPF_RESULT_NONE ) {
	    ret = SPF_RESULT_PERMERROR;
	}

    } else {
	/* RFC 7208 4.7 Default Result
	 * If none of the mechanisms match and there is no "redirect" modifier,
	 * then the check_host() returns a result of "neutral", just as if
	 * "?all" were specified as the last directive.
	 */
	ret = SPF_RESULT_NEUTRAL;
	simta_debuglog( 2, "SPF %s [%s]: default result: %s", s->spf_domain,
		domain, spf_result_str( ret ));
    }

cleanup:
    if ( split != NULL ) {
	yaslfreesplitres( split, tok_count );
    }
    yaslfree( record );
    dnsr_free_result( dnsr_res );
    return( ret );
}