[einfach bis mittel] E-Mail-Adresse auf Gültigkeit prüfen

CDW

Moderator
Mitarbeiter
#1
OT:
Es überrascht mich, dass es diese Aufgabe scheinbar noch nicht gab.
--
Eingereicht von Christian216.

Schreibe ein Programm, welches in der Lage ist eine E-Mail-Adresse zu validieren.
Dabei sollte auf die Verwendung von regulären Ausdrücken, sei es als Bibliothek oder Sprachmittel, verzichtet werden.

Stattdessen könnte man sich bei den endlichen Automaten (deterministisch, nichtdeterministisch, Umwandlung von nichtdeterministischen endlichen Automaten zu deterministischen, Umsetzung des Automaten als Code) umsehen.

Wer mag, validiert alternativ (oder zusätzlich) IP-Adressen (IPv4 und/oder IPv6).
--
Wie immer gilt: ob GUI oder Konsole bleibt euch überlassen. Ebenso wie die Daten eingelesen werden - aus einer Datei, als Kommandozeilenparameter, aus dem Benutzerinterface oder per SMS.
 

Chromatin

Moderator
Mitarbeiter
#3
Hier auch mal ein POC zusammengedengelt :)


Code:
#!/usr/bin/perl

my $addr = shift;

die "your address is too long .. " if (length($addr) > 254); # zu lang

## map gueltige zeichen auf hashref .. qw ginge auch :P
my %h = map {$_, => '1' } ('a','b','c','d','e','f','g',
'h','i','j','k','l','m','n','o','p','q','r','s','t','u',
'v','w','x','y','z','A','B','C','D','E','F','G','H','I',
'J','K','L','M','N','O','P','Q','R','S','T','U','V','W',
'X','Y','Z','.','-','0','1','2','3','4','5','6','7','8','9');

my @arr        = split(/@/, $addr);      # local und global splitten 
die "Kein oder zuviele @ :P" if (scalar(@arr) != 2);

my @localteil  = split(/\./, @arr[0]);   # local Teil
die "localteil zu kurz (minimum 1)" if (length(@arr[0]) <1);

foreach my $ln (split(/\./, @arr[1]))
{
  push @domain, $ln if ($ln);
}
 
die "Globalteil muss mindestens Format a.bc haben" if  (scalar (@domain < 2));
die "TLD zu kurz (minimum 2)" if ( length($domain[$#domain]) < 2);
die "TLD zu lang (max 63)" if ( length($domain[$#domain]) > 63);

## wenn hier alles tutti, Eingabeadresse (ohne @) auf gueltige Zeichen checken 

$ad = $arr[0] . $arr[1];

foreach my $zeichen  (split(//,$ad))
{
   die "UngueltigesZeichen $zeichen\n"  if ( %h{$zeichen} < 1 );
}
 
print "Email Adresse Tutti!\n";
@CDW

Ein realistischer Validator der absolut RFC5322 und 6531 konform sein soll, wird ohne Regex sicher den Traffic sprengen :D
 
#4
@Chromatin

Nah was sehen meine entzündeten Augen denn da... :rolleyes:
Haben wir nicht gesagt keine Verarbeitung regulärer Ausdrücke nutzen? :wink:

Problem ist aber das du nicht auf gültigen Abschluss mit .de , .net , .com usw. testen...
Die E-Mail Adresse muss ja nicht nur von der Art und Anzahl der verwendeten Zeichen korrekt sein, sondern auch syntaktisch.

Liebe Grüße

Christian
 

Chromatin

Moderator
Mitarbeiter
#5
Problem ist aber das du nicht auf gültigen Abschluss mit .de , .net , .com usw. testen...
Was meinst du damit?
Der Top-Level kann 2-63 Zeichen lang sein (die maximal Länge habe ich noch hinzugefügt).

Haben wir nicht gesagt keine Verarbeitung regulärer Ausdrücke nutzen?
Ich habe das als auf direktes matching bezogen verstanden.. sonst müsste man auch eine split() Funktion meiden ;)
 

Chromatin

Moderator
Mitarbeiter
#7
Ich habe jetzt die beiden Regex rausgenommen..wobei nur die erste dem matching diente :)

PS: Das Teil hat leider einen Bug...aber vielleicht findet den ja keiner :p
 

SchwarzeBeere

Moderator
Mitarbeiter
#8
Meine Lösung aufwändig als Automat mit unterschiedlichen Zuständen und Zustandsübergängen - aber dafür leicht adaptierbar, wie das Beispiel mit den IP-Adressen zeigen sollte ;)

Code:
package matchmail;

import java.util.function.BiPredicate;
import java.util.function.Predicate;

public class EmailAddressValidator {

	public static void main(String[] args) {

		// Sample input
		String email = "t+ietf-spec@sdfg.de";

		// Rules for validation
		String validChars1 = "abcdefghijklmnopqrstuvwxyz1234567890";
		String validChars2 = "abcdefghijklmnopqrstuvwxyz1234567890.-_";
		
		Predicate<Message> validCharsAlphNum = m -> m.getCurrentMessage().chars().allMatch(c -> validChars1.indexOf(c) != -1);
		Predicate<Message> validCharsAlphNumSpec = m -> m.getCurrentMessage().chars().allMatch(c -> validChars2.indexOf(c) != -1);
		Predicate<Message> validCharsAlphNumSpecPlus = m -> m.getCurrentMessage().chars().allMatch(c -> (validChars2+"+").indexOf(c) != -1);
		Predicate<Message> maxPlusInAddr = m -> m.getCurrentMessage().chars().mapToLong(c -> (c == '+')?1:0).sum()<=1;
		Predicate<Message> minAlph = m -> m.getCurrentMessage().chars().mapToLong(c -> (validChars1.indexOf(c)!=-1)?1:0).sum()>0;
		Predicate<Message> tldLenMin = m -> m.getCurrentMessage().length() > 1;
		Predicate<Message> tldLenMax = m -> m.getCurrentMessage().length() < 64;

		// States combine different rules to validate different parts of the address
		State state0 = new State(validCharsAlphNumSpecPlus.and(maxPlusInAddr).and(minAlph));
		State state1 = new State(validCharsAlphNumSpec.and(minAlph));
		State state2 = new State(tldLenMax.and(tldLenMin).and(validCharsAlphNum)); //  chars of string up to first "." are allowed

		// Transitions defined as relationships between input and current state of processing
		BiPredicate<String, Message> state0to1 = (s, m) -> m.getCurrentMessage().charAt(m.getCurrentMessage().length() - 1) == '@';
		BiPredicate<String, Message> state1to2 = (s, m) -> s.indexOf(".",s.indexOf(m.getCurrentMessage()) + m.getCurrentMessage().length()) == -1;

		// Create graph and transitions
		Graph g = new Graph();
		g.addStartState(state0);
		g.addTransition(state0, state1, state0to1);
		g.addTransition(state1, state2, state1to2);

		// Validate mail
		if (g.validate(email))
			System.out.println("Address is valid.");
		else
			System.out.println("Address is invalid.");

	}
}
Code:
package matchmail;

import java.util.HashMap;
import java.util.function.BiPredicate;

public class Graph {

    public HashMap<Integer, State> transitions = new HashMap<Integer, State>();

    public HashMap<Integer, BiPredicate<String, Message>> conditions = new HashMap<Integer, BiPredicate<String, Message>>();

    private int currentState = 0;

    public void addStartState(State state) {
        transitions.put(0, state);
    }

    public void addTransition(State state1, State state2, BiPredicate<String, Message> condition) {
        transitions.put(state1.hashCode(), state2);
        conditions.put(state1.hashCode(), condition);
    }

    public boolean validate(String input) {
        
        State current = transitions.get(currentState);
        BiPredicate<String, Message> transCond = conditions.get(current.hashCode());

        for (char c : input.toCharArray()) {
            
            System.out.println("State: " + current.hashCode() +" Char: "+c);

            current.update(c);
            
            if ((transCond != null && transCond.test(input, current.getOutput()))) {
                
                // remove last char, as it may not be valid anymore
                current.resetLastUpdate();
                
                // Validate
                if (!current.valid())
                    return false;
                
                // Get next state
                Message output = current.getOutput();
                current = transitions.get(current.hashCode());
                current.setInput(output);

                // Get next transition condition
                transCond = conditions.get(current.hashCode());
                
            }

        }

        //System.out.println("Address is "
        //        + ((current.valid() && transitions.get(current.hashCode()) == null) ? "valid" : "invalid"));

        if (transCond == null) {
            return current.valid();
        }
        return false;
    }
}
Code:
package matchmail;

import java.util.function.Predicate;

public class State {

    private StringBuilder intState = new StringBuilder();

    private Message intStateMsg = new Message();

    Predicate<Message> intValidator;

    public State(Predicate<Message> validator) {
        intValidator = validator;
    }

    public void setInput(Message m) {
        intStateMsg.setInput(m);
    }

    public void update(char c) {
        intState.append(c);
        intStateMsg.setCurrentMessage(intState.toString());
    }

    public boolean valid() {
        try {
            System.out.println("Past=" + intStateMsg.getInput() + " Current="
                    + intStateMsg.getCurrentMessage());
            return intValidator.test(intStateMsg);
        }
        catch (Exception e) {
            return false;
        }
    }

    public Message getOutput() {
        return intStateMsg;
    }

    public void resetLastUpdate() {
        intState.deleteCharAt(intState.length() - 1);
        intStateMsg.setCurrentMessage(intState.toString());
    }

}
Code:
package matchmail;

public class Message {
    
    private String receivedState = "";
    private String currentState = "";
    
    public void setInput(Message processedString) {
        this.receivedState = processedString.getCurrentMessage();
    }
    
    public void setCurrentMessage(String charsLeft) {
        this.currentState = charsLeft;
    }
    
    public String getCurrentMessage() {
        return this.currentState;
    }
    
    public String getInput() {
        return this.receivedState;
    }
    
    

}
Code:
package matchmail;

import java.util.function.BiPredicate;
import java.util.function.Predicate;

public class IPAddressValidator {

    public static void main(String[] args) {
        // Sample input
        String ip = "192.168.1.20";

        // Rules for validation
        Predicate<Message> len = m -> m.getCurrentMessage().length() > 0 && m.getCurrentMessage().length() < 4;
        Predicate<Message> size = m -> Integer.parseInt(m.getCurrentMessage()) >= 0 && Integer.parseInt(m.getCurrentMessage()) < 256;

        // States combine different rules to validate different parts of the address
        State state0 = new State(len.and(size));
        State state1 = new State(len.and(size));
        State state2 = new State(len.and(size));
        State state3 = new State(len.and(size));

        // Transitions defined as relationships between input and current state of processing
        BiPredicate<String, Message> defaultTransition = (s, m) -> m.getCurrentMessage().charAt(
                m.getCurrentMessage().length() - 1) == '.';

        // Create graph and transitions
        Graph g = new Graph();
        g.addStartState(state0);
        g.addTransition(state0, state1, defaultTransition);
        g.addTransition(state1, state2, defaultTransition);
        g.addTransition(state2, state3, defaultTransition);

        // Validate addr
        if (g.validate(ip))
            System.out.println("Address is valid.");
        else
            System.out.println("Address is invalid.");

    }

}

... und es kommt ohne spilt() aus ;)
 
Zuletzt bearbeitet:
#9
Mein Code kommt auch ganz ohne split() und regexen aus.
Dafür muss ich zugeben, dass es nicht 100%ig RFC kompatibel ist. Was nicht unterstützt wird:
- Anführungszeichen im local part
- kommentare
- IP-Addresse als Domain part

Sollte noch machbar sein, aber momentan bin ich schon recht stolz darauf.
Das Programm ist in Assembler für Linux geschrieben. Es sind aber nur 2 kleine
Funktionen plattformabhängig, also ist es leicht zu portieren.

Code:
format ELF executable

sys_exit    equ     1
sys_read    equ     3
sys_write   equ     4
stdin       equ     0
stdout      equ     1
stderr      equ     2

entry _start

segment executable

; --- linux specific ---

; _noreturn program_end()
program_end:
	mov eax, sys_exit
	xor ebx, ebx
	int 80h

; void print(char *s)
; s should be on the stack
print:
	push ebp
	mov ebp, esp
	push esi
	mov esi, [ebp+8]
	call strlen

	mov edx, eax
	mov ecx, esi
	mov ebx, stdout
	mov eax, sys_write
	int 80h

	pop esi
	leave
	ret

; --- platform independent ---

; int strlen(char *s)
; s should be in esi
; returns the strings length in eax
strlen:
	push edi
	mov edi, esi

	xor al, al
	mov ecx, -1
	repne scasb

	inc ecx
	neg ecx

	mov eax, ecx

	pop edi
	ret

; int strchr(char *s, char x)
; s should be in esi, x in dl
; returns position of first occurence of x
; in s (or -1 if there is none)
strchr:
	push esi
	test dl, dl
	jz .notfound

	xor ecx, ecx
@@:
	lodsb
	test al, al
	jz .notfound
	inc ecx
	
	cmp al, dl
	je .exit

	jmp @b

.notfound:
	xor ecx, ecx
.exit:
	mov eax, ecx
	dec eax
	pop esi
	ret

; int strcmp(char *s1, char *s2)
; s1 in esi, s2 in edi
; returns 0 if the strings are equal
; nonzero otherwise
stricmp:
	push esi
	push edi

	xor eax, eax
@@:
	mov al, [esi]
	mov ah, [edi]
	inc esi
	inc edi

	cmp al, 'a'
	jb .alok
	cmp al, 'z'
	ja .alok
	and al, not 32
.alok:
	cmp ah, 'a'
	jb .ahok
	cmp ah, 'z'
	ja .ahok
	and ah, not 32
.ahok:
	cmp al, ah
	jne .notequal

	test al, al
	jz .equal

	jmp @b
.equal:
	xor eax, eax
	jmp .exit
.notequal:
	mov al, 1
.exit:
	pop edi
	pop esi
	ret


; tld_verify(char *tld)
; tld should be in esi
; returns 0 in eax if tld is valid
; and non-zero otherwise
tld_verify:
	push esi
	mov ecx, [tld_count]
@@:
	mov edi, [tld_list+ecx*4-4]
	push ecx
	call stricmp
	pop ecx
	test eax, eax
	jz .oktld
	loop @b

	mov al, 1
	jmp .exit
.oktld:
	xor eax, eax
.exit:
	pop esi
	ret

; email_verify(char *s)
; s should be in esi
; returns 0 in eax if e-mail address is syntactically correct
; and non-zero otherwise
email_verify:
	push ebp
	mov ebp, esp
	sub esp, 24

	xor ecx, ecx ; total length count

	mov [ebp-4], esi
	mov [ebp-8], edi
	mov [ebp-16], ecx ; <-- save last character
	mov [ebp-24], ecx ; <-- store tld

.localpart:
	lodsb
	
	test al, al
	jz .incorrect

	cmp al, '@'
	je .localpart_exit
	
	cmp ecx, 64
	jge .incorrect

	cmp al, '.'
	jne .localpart_nodot
	test ecx, ecx
	jz .incorrect ; no dot as first character
	xchg eax,[ebp-16]
	cmp al, '.'
	je .incorrect ; dot may not follow dot
	jmp .localpart
.localpart_nodot:
	
	inc ecx

	mov [ebp-12], ecx
	mov [ebp-16], eax
	mov edi, esi
	mov dl, al
	mov esi, localpart_chars
	call strchr
	mov esi, edi

	mov ecx, [ebp-12]

	test eax, eax
	js .incorrect

	; character was ok

	jmp .localpart
.localpart_exit:
	cmp ecx, 64 ; local part maximum length is 64 characters
	jg .incorrect

	mov eax, [ebp-16]
	cmp al, '.'
	je .incorrect ; dot cannot be last character in local part

	inc ecx

	xor edx, edx
.domainpart:
	lodsb

	test al, al
	jz .domainpart_exit

	cmp edx, 64
	jge .incorrect

	cmp al, '-'
	jne .domainpart_nohyphen
	test edx, edx
	jz .incorrect ; hyphen may not start a domain segment
.domainpart_nohyphen:

	cmp al, '.'
	jne .domainpart_nodot
	test edx, edx
	jz .incorrect ; no dot as first character
	xchg eax, [ebp-16]
	cmp al, '-' ; hyphen may not end a domain segment
	je .incorrect

	mov [ebp-24], esi
	xor edx, edx
	jmp .domainpart
.domainpart_nodot:
	
	inc ecx
	inc edx

	mov [ebp-12], ecx
	mov [ebp-16], eax
	mov [ebp-20], edx
	mov edi, esi
	mov dl, al
	mov esi, domainpart_chars
	call strchr
	mov esi, edi

	mov ecx, [ebp-12]
	mov edx, [ebp-20]

	test eax, eax
	js .incorrect

	jmp .domainpart
.domainpart_exit:
	test edx, edx
	jz .incorrect

	cmp ecx, 256
	jg .incorrect

	mov esi, [ebp-24]
	test esi, esi
	jz @f ; <- no tld
	call tld_verify
	test eax, eax
	jnz .incorrect ; <- invalid tld
@@:

.correct:
	xor eax, eax
	jmp .exit

.incorrect:
	mov eax, 1

.exit:
	mov edi, [ebp-8]
	mov esi, [ebp-4]
	leave
	ret

; program entry point
_start:
	nop
	push ebp
	mov ebp, esp
	cmp dword [ebp+4], 2
	je .args_correct

	; incorrect number of arguments
	push cmd_args_s
	call print
	add esp, 4
	jmp .exit

.args_correct:
	mov esi, [ebp+12]
	call email_verify

	test eax, eax
	jnz .verify_error
; verify correct
	push email_ok_s
	jmp .showstr
.verify_error:
	push email_mal_s
.showstr:
	call print
	add esp,4

.exit:
	leave
	jmp program_end


segment readable

cmd_args_s db 'call with exactly one argument!',10,0
email_ok_s db 'e-mail address syntax is correct!',10,0
email_mal_s db 'malformed e-mail address!',10,0
localpart_chars:
db "!#$%&'*+/=?^_`{|}~;"
domainpart_chars:
db "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-",0

include 'tlds.asm'

Es includet eine Datei namens 'tlds.asm' die man mit folgendem python skript generieren kann:
Code:
import sys
		
def q(s):
	return "\""+str(s)+"\""

class TLD:
	def __init__(self, s):
		self.s = s
		self.split = [0]

	def checksplit(self, s):
		if self.s.endswith(s):
			self.split.append(len(self.s)-len(s)-1)
			return True
		return False

	def echo(self,name,i):
		self.split.sort()
		s = self.s
		minus = 0
		for j in range(0,len(self.split)):
			print name+str(i)+":"
			i+=1
			if len(self.split) <= j+1:
				print "db",q(s),",0"
				break
			n=self.split[j+1]-minus
			print "db", q(s[:n+1])
			s = s[n+1:]
			minus+=n+1
		return i
		
		

tld_strings=[]

for line in sys.stdin:
	tld_strings.append(line.strip())

tld_strings.sort(lambda x,y:-cmp(len(x),len(y)))
count = len(tld_strings)

tlds=[]
for line in tld_strings:
	appended = False
	for t in tlds:
		if t.checksplit(line):
			appended = True
			break
	if not appended:
		tlds.append(TLD(line))


print "tld_count dd",count

i = 0
for t in tlds:
	i = t.echo("s_tld",i)

print "tld_list:"
for i in range(0, count):
	print "dd s_tld"+str(i)
Einfach eine Liste aller TLDs in standard input des skripts pipen und output in tlds.asm speichern.

Zum assemblieren braucht ihr den flat assembler.
Die ausführbare Datei ist bei mir 14.583 Byte groß (das meiste davon sind die TLDs) :D

Wer Fehler findet oder Verbesserungsvorschläge hat, darf sich gerne melden!
Habe das ganze auch auf github gestellt: assembly-code/e-mail-verify at master * chtisgit/assembly-code * GitHub
 
Oben