ANSI C89/ISO C90縛りでメールアドレスのフォーマットチェック関数を書いてみた

タイトル通り、必死こいて汗水垂らしながら実装した。zlib/libpng ライセンスで公開する。

/*
 * Copyright (c) 2010 eel3 @ TRASH BOX <dov045a@yahoo.co.jp>
 *
 * This software is provided 'as-is', without any express or implied
 * warranty. In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 *     1. The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software
 *     in a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *
 *     2. Altered source versions must be plainly marked as such, and must not be
 *     misrepresented as being the original software.
 *
 *     3. This notice may not be removed or altered from any source
 *     distribution.
 */

/* ********************************************************************** */
/**
 * @brief   メールアドレスのフォーマットチェック関数 && テスト用メインルーチン
 * @author  eel3 @ TRASH BOX
 * @date    2010/01/30
 *
 * @note
 *   メールアドレスが RFCで規定されている形式に従っているかチェックする。
 *   但し Quoted-string, address-literal には未対応。
 *   あとフォーマットをチェックしているだけなので、現実に無さそうな内容
 *   (例えば a@b.c)でもOKと判定してしまう。
 *
 * @par 動作確認済み環境:
 *   - Microsoft Windows XP Professional (32bit) SP3
 *
 * @par 確認済みコンパイラ:
 *   - Digital Mars C and C++ Compilers 8.49
 *   - Microsoft Visual Studio 6.0
 *   - Microsoft Visual Studio 2005
 *   - MinGW 5.0.3 (GCC 3.4.2)
 */
/* ********************************************************************** */


#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>


/* ====================================================================== */
/**
 * @brief  [a-zA-Z] なアルファベット文字かチェックする。
 *
 * @param[in] c  チェックする文字
 *
 * @retval !=0  アルファベット文字である
 * @retval   0  アルファベット文字ではない
 *
 * @note
 *   標準ライブラリの isalpha(3) はロケールに左右されるので、
 *   自前で判定関数を定義している。
 */
/* ====================================================================== */
static int my_isalpha(int c)
{
	return ((c != '\0')
	        && (strchr("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", c) != NULL));
}

/* ====================================================================== */
/**
 * @brief  [a-zA-Z] なアルファベット文字ないし数字かチェックする。
 *
 * @param[in] c  チェックする文字
 *
 * @retval !=0  アルファベット文字か数字である
 * @retval   0  アルファベット文字か数字ではない
 *
 * @note
 *   標準ライブラリの isalnum(3) はロケールに左右されるので、
 *   自前で判定関数を定義している。
 */
/* ====================================================================== */
static int my_isalnum(int c)
{
	return (my_isalpha(c) || isdigit(c));
}

/* ====================================================================== */
/**
 * @brief  メールアドレスのLocal-partに使用可能な文字かチェックする。
 *
 * @param[in] c  チェックする文字
 *
 * @retval !=0  使用できる
 * @retval   0  使用できない
 */
/* ====================================================================== */
static int islocalc(int c)
{
	return (my_isalnum(c)
	        || ((c != '\0') && (strchr(".!#$%&\'*+-/=\?^_`{|}~", c) != NULL)));
}

/* ====================================================================== */
/**
 * @brief  メールアドレスのLocal-partかどうかチェックする。
 *
 * @param[in] *s    チェックする文字列
 * @param[in] size  文字列の長さ(単位:byte)
 *
 * @retval !=0  Local-partである
 * @retval   0  Local-partではない
 */
/* ====================================================================== */
static int islocalpart(const char *s, size_t size)
{
	size_t i;
	int dot_found;      /* 直前の文字が . だったら1 */

	assert(s != NULL);

	if ((s[0] == '\0') || (size == 0)) {
		return 0;
	}

	if (size > 64) {    /* RFC 5321  4.5.3.1.1 */
		return 0;
	}

	if (s[0] == '.') {
		return 0;
	}

	dot_found = 0;
	for (i = 0; (i < size) && (s[i] != '\0'); ++i) {
		if (!islocalc((unsigned char) s[i])) {
			return 0;
		}

		if (s[i] == '.') {
			if (dot_found) {
				return 0;
			} else {
				dot_found = 1;
			}
		} else {
			dot_found = 0;
		}
	}

	return (s[i-1] != '.');
}

/* ====================================================================== */
/**
 * @brief  メールアドレスのsub-domainに使用可能な文字かチェックする。
 *
 * @param[in] c  チェックする文字
 *
 * @retval !=0  使用できる
 * @retval   0  使用できない
 */
/* ====================================================================== */
static int issubdomainc(int c)
{
	return (my_isalnum(c) || (c == '-'));
}

/* ====================================================================== */
/**
 * @brief  メールアドレスのsub-domainかどうかチェックする。
 *
 * @param[in] *s    チェックする文字列
 * @param[in] size  文字列の長さ(単位:byte)
 *
 * @retval !=0  sub-domainである
 * @retval   0  sub-domainではない
 */
/* ====================================================================== */
static int issubdomain(const char *s, size_t size)
{
	size_t i;

	assert(s != NULL);

	if ((s[0] == '\0') || (size == 0)) {
		return 0;
	}

	if (size > 63) {    /* RFC 1035  2.3.1 */
		return 0;
	}

	if (!my_isalpha((unsigned char) s[0])) {
		return 0;
	}

	for (i = 1; (i < size) && (s[i] != '\0'); ++i) {
		if (!issubdomainc((unsigned char) s[i])) {
			return 0;
		}
	}

	return (s[i-1] != '-');
}

/* ====================================================================== */
/**
 * @brief  メールアドレスのDomainかどうかチェックする。
 *
 * @param[in]  *s     チェックする文字列
 * @param[out] *size  Domainの長さを返す
 *
 * @retval !=0  Domainである
 * @retval   0  Domainではない
 */
/* ====================================================================== */
static int isdomain(const char *s, size_t *size)
{
	const char *p, *sepp;
	size_t nsubdomains;

	assert(s != NULL);

	if (s[0] == '\0') {
		return 0;
	}

	nsubdomains = 0;
	p = s;

	do {
		sepp = p + strcspn(p, ".");
		if (!issubdomain(p, (size_t) (sepp - p))) {
			return 0;
		}
		p = sepp+1;
		++nsubdomains;

	} while (*sepp == '.');

	*size = (size_t) (p - s);
	if (*size > 255) {      /* RFC 5321  4.5.3.1.2 */
		return 0;
	}

	return (nsubdomains >= 2);
}

/* ====================================================================== */
/**
 * @brief  メールアドレスとして適切なフォーマットかチェックする。
 *
 * @param[in] *s  チェックする文字列
 *
 * @retval !=0  適切である
 * @retval   0  適切ではない
 *
 * @par 参考資料
 * - RFC 1035, RFC 5321, RFC 5322
 * - http://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9
 *
 * @note
 *   メールアドレスの形式: Local-part@(Domain|address-literal)
 *
 * @attention
 * - 前後の空白文字はトリミングされていること。
 * - Local-partはDot-stringにのみ対応。Quoted-string には対応しない。
 * - Domainにのみ対応。address-literalには対応しない。
 */
/* ====================================================================== */
static int isemailaddr(const char *s)
{
	const char *atmk;
	size_t lp_size, dmn_size;

	assert(s != NULL);

	if (s[0] == '\0') {
		return 0;
	}

	/* Quoted-string には対応しない */
	if (s[0] == '\"') {
		return 0;
	}

	atmk = strchr(s, '@');
	if (atmk == NULL) {
		return 0;
	}

	lp_size = (size_t) (atmk - s);
	if (!islocalpart(s, lp_size)) {
		return 0;
	}
	if (!isdomain(atmk+1, &dmn_size)) {
		return 0;
	}

	return ((lp_size + dmn_size) <= 256);   /* RFC 5321  4.5.3.1.3 */
}


/* ********************************************************************** */
/**
 * @brief  テスト用メインルーチン
 *
 * @return  常に0
 */
/* ********************************************************************** */
int main(void)
{
	static char buf[1024+1];

	while (fgets(buf, (int) sizeof(buf), stdin) != NULL) {
		size_t len = strlen(buf);

		if (buf[len-1] == '\n') {
			buf[len-1] = '\0';
		}
		(void) puts(isemailaddr(buf) ? "OK" : "NG");
	}

	return 0;
}

ろくにテストしていないので、取り扱い注意。